mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 19:16:24 +00:00
feat: 添加评论功能的客户端信息显示选项,更新相关接口和组件
This commit is contained in:
@ -1,47 +1,48 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
type CommentDto struct {
|
type CommentDto struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
TargetID uint `json:"target_id"`
|
TargetID uint `json:"target_id"`
|
||||||
TargetType string `json:"target_type"` // 目标类型,如 "post", "page"
|
TargetType string `json:"target_type"` // 目标类型,如 "post", "page"
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
ReplyID uint `json:"reply_id"` // 回复的评论ID
|
ReplyID uint `json:"reply_id"` // 回复的评论ID
|
||||||
Depth int `json:"depth"` // 评论的层级深度
|
Depth int `json:"depth"` // 评论的层级深度
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
User UserDto `json:"user"` // 评论的
|
User UserDto `json:"user"` // 评论的
|
||||||
ReplyCount uint64 `json:"reply_count"` // 回复数量
|
ReplyCount uint64 `json:"reply_count"` // 回复数量
|
||||||
LikeCount uint64 `json:"like_count"` // 点赞数量
|
LikeCount uint64 `json:"like_count"` // 点赞数量
|
||||||
IsLiked bool `json:"is_liked"` // 当前用户是否点赞
|
IsLiked bool `json:"is_liked"` // 当前用户是否点赞
|
||||||
IsPrivate bool `json:"is_private"`
|
IsPrivate bool `json:"is_private"`
|
||||||
Location string `json:"location"` // 用户位置,基于IP
|
Location string `json:"location"` // 用户位置,基于IP
|
||||||
OS string `json:"os"` // 用户操作系统,基于User-Agent
|
OS string `json:"os"` // 用户操作系统,基于User-Agent
|
||||||
Browser string `json:"browser"` // 用户浏览器,基于User-Agent
|
Browser string `json:"browser"` // 用户浏览器,基于User-Agent
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateCommentReq struct {
|
type CreateCommentReq struct {
|
||||||
TargetID uint `json:"target_id" binding:"required"` // 目标ID
|
TargetID uint `json:"target_id" binding:"required"` // 目标ID
|
||||||
TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "page"
|
TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "page"
|
||||||
Content string `json:"content" binding:"required"` // 评论内容
|
Content string `json:"content" binding:"required"` // 评论内容
|
||||||
ReplyID uint `json:"reply_id"` // 回复的评论ID
|
ReplyID uint `json:"reply_id"` // 回复的评论ID
|
||||||
IsPrivate bool `json:"is_private"` // 是否私密评论,默认false
|
IsPrivate bool `json:"is_private"` // 是否私密评论,默认false
|
||||||
RemoteAddr string `json:"remote_addr"` // 远程地址
|
RemoteAddr string `json:"remote_addr"` // 远程地址
|
||||||
UserAgent string `json:"user_agent"` // 用户代理
|
UserAgent string `json:"user_agent"` // 用户代理
|
||||||
|
ShowClientInfo bool `json:"show_client_info"` // 是否显示客户端信息
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateCommentReq struct {
|
type UpdateCommentReq struct {
|
||||||
CommentID uint `json:"comment_id" binding:"required"` // 评论ID
|
CommentID uint `json:"comment_id" binding:"required"` // 评论ID
|
||||||
Content string `json:"content" binding:"required"` // 评论内容
|
Content string `json:"content" binding:"required"` // 评论内容
|
||||||
IsPrivate bool `json:"is_private"` // 是否私密
|
IsPrivate bool `json:"is_private"` // 是否私密
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetCommentListReq struct {
|
type GetCommentListReq struct {
|
||||||
TargetID uint `json:"target_id" binding:"required"`
|
TargetID uint `json:"target_id" binding:"required"`
|
||||||
TargetType string `json:"target_type" binding:"required"`
|
TargetType string `json:"target_type" binding:"required"`
|
||||||
CommentID uint `json:"comment_id"` // 获取某条评论的所有子评论
|
CommentID uint `json:"comment_id"` // 获取某条评论的所有子评论
|
||||||
OrderBy string `json:"order_by"` // 排序方式
|
OrderBy string `json:"order_by"` // 排序方式
|
||||||
Page uint64 `json:"page"` // 页码
|
Page uint64 `json:"page"` // 页码
|
||||||
Size uint64 `json:"size"`
|
Size uint64 `json:"size"`
|
||||||
Desc bool `json:"desc"`
|
Desc bool `json:"desc"`
|
||||||
Depth int `json:"depth"` // 评论的层级深度
|
Depth int `json:"depth"` // 评论的层级深度
|
||||||
}
|
}
|
||||||
|
@ -7,19 +7,20 @@ import (
|
|||||||
|
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
UserID uint `gorm:"index"` // 评论的用户ID
|
UserID uint `gorm:"index"` // 评论的用户ID
|
||||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||||
TargetID uint `gorm:"index"` // 目标ID
|
TargetID uint `gorm:"index"` // 目标ID
|
||||||
TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
|
TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
|
||||||
ReplyID uint `gorm:"index"` // 回复的评论ID
|
ReplyID uint `gorm:"index"` // 回复的评论ID
|
||||||
Content string `gorm:"type:text"` // 评论内容
|
Content string `gorm:"type:text"` // 评论内容
|
||||||
Depth int `gorm:"default:0"` // 评论的层级深度,从0开始计数
|
Depth int `gorm:"default:0"` // 评论的层级深度,从0开始计数
|
||||||
IsPrivate bool `gorm:"default:false"` // 是否为私密评论,私密评论只有评论者和被评论对象所有者可见
|
IsPrivate bool `gorm:"default:false"` // 是否为私密评论,私密评论只有评论者和被评论对象所有者可见
|
||||||
RemoteAddr string `gorm:"type:text"` // 远程地址
|
RemoteAddr string `gorm:"type:text"` // 远程地址
|
||||||
UserAgent string `gorm:"type:text"`
|
UserAgent string `gorm:"type:text"`
|
||||||
Location string `gorm:"type:text"` // 用户位置,基于IP
|
Location string `gorm:"type:text"` // 用户位置,基于IP
|
||||||
LikeCount uint64
|
LikeCount uint64
|
||||||
CommentCount uint64
|
CommentCount uint64
|
||||||
|
ShowClientInfo bool `gorm:"default:false"` // 是否显示客户端信息
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Comment) AfterCreate(tx *gorm.DB) (err error) {
|
func (c *Comment) AfterCreate(tx *gorm.DB) (err error) {
|
||||||
|
@ -121,6 +121,7 @@ func (cr *CommentRepo) CreateComment(comment *model.Comment) (uint, error) {
|
|||||||
})
|
})
|
||||||
return commentID, err
|
return commentID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *CommentRepo) UpdateComment(comment *model.Comment) error {
|
func (cr *CommentRepo) UpdateComment(comment *model.Comment) error {
|
||||||
if comment.ID == 0 {
|
if comment.ID == 0 {
|
||||||
return errs.New(http.StatusBadRequest, "invalid comment ID", nil)
|
return errs.New(http.StatusBadRequest, "invalid comment ID", nil)
|
||||||
|
@ -1,184 +1,213 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/snowykami/neo-blog/pkg/constant"
|
"github.com/snowykami/neo-blog/pkg/constant"
|
||||||
"github.com/snowykami/neo-blog/pkg/utils"
|
"github.com/snowykami/neo-blog/pkg/utils"
|
||||||
|
|
||||||
"github.com/snowykami/neo-blog/internal/ctxutils"
|
"github.com/snowykami/neo-blog/internal/ctxutils"
|
||||||
"github.com/snowykami/neo-blog/internal/dto"
|
"github.com/snowykami/neo-blog/internal/dto"
|
||||||
"github.com/snowykami/neo-blog/internal/model"
|
"github.com/snowykami/neo-blog/internal/model"
|
||||||
"github.com/snowykami/neo-blog/internal/repo"
|
"github.com/snowykami/neo-blog/internal/repo"
|
||||||
"github.com/snowykami/neo-blog/pkg/errs"
|
"github.com/snowykami/neo-blog/pkg/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommentService struct{}
|
type CommentService struct{}
|
||||||
|
|
||||||
func NewCommentService() *CommentService {
|
func NewCommentService() *CommentService {
|
||||||
return &CommentService{}
|
return &CommentService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateCommentReq) (uint, error) {
|
func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateCommentReq) (uint, error) {
|
||||||
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, errs.ErrUnauthorized
|
return 0, errs.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := cs.checkTargetExists(req.TargetID, req.TargetType); !ok {
|
if ok, err := cs.checkTargetExists(req.TargetID, req.TargetType); !ok {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errs.New(errs.ErrBadRequest.Code, "target not found", err)
|
return 0, errs.New(errs.ErrBadRequest.Code, "target not found", err)
|
||||||
}
|
}
|
||||||
return 0, errs.ErrBadRequest
|
return 0, errs.ErrBadRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
comment := &model.Comment{
|
comment := &model.Comment{
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
ReplyID: req.ReplyID,
|
ReplyID: req.ReplyID,
|
||||||
TargetID: req.TargetID,
|
TargetID: req.TargetID,
|
||||||
TargetType: req.TargetType,
|
TargetType: req.TargetType,
|
||||||
UserID: currentUser.ID,
|
UserID: currentUser.ID,
|
||||||
IsPrivate: req.IsPrivate,
|
IsPrivate: req.IsPrivate,
|
||||||
RemoteAddr: req.RemoteAddr,
|
RemoteAddr: req.RemoteAddr,
|
||||||
UserAgent: req.UserAgent,
|
UserAgent: req.UserAgent,
|
||||||
}
|
ShowClientInfo: req.ShowClientInfo,
|
||||||
|
}
|
||||||
|
|
||||||
commentID, err := repo.Comment.CreateComment(comment)
|
commentID, err := repo.Comment.CreateComment(comment)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return commentID, nil
|
return commentID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateCommentReq) error {
|
func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateCommentReq) error {
|
||||||
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errs.ErrUnauthorized
|
return errs.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID)))
|
comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.ID != comment.UserID {
|
if currentUser.ID != comment.UserID {
|
||||||
return errs.ErrForbidden
|
return errs.ErrForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
comment.Content = req.Content
|
comment.Content = req.Content
|
||||||
comment.IsPrivate = req.IsPrivate
|
comment.IsPrivate = req.IsPrivate
|
||||||
|
|
||||||
err = repo.Comment.UpdateComment(comment)
|
err = repo.Comment.UpdateComment(comment)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) error {
|
func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) error {
|
||||||
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errs.ErrUnauthorized
|
return errs.ErrUnauthorized
|
||||||
}
|
}
|
||||||
if commentID == "" {
|
if commentID == "" {
|
||||||
return errs.ErrBadRequest
|
return errs.ErrBadRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
comment, err := repo.Comment.GetComment(commentID)
|
comment, err := repo.Comment.GetComment(commentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.New(errs.ErrNotFound.Code, "comment not found", err)
|
return errs.New(errs.ErrNotFound.Code, "comment not found", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment.UserID != currentUser.ID {
|
if comment.UserID != currentUser.ID {
|
||||||
return errs.ErrForbidden
|
return errs.ErrForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo.Comment.DeleteComment(commentID); err != nil {
|
if err := repo.Comment.DeleteComment(commentID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dto.CommentDto, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, errs.New(errs.ErrNotFound.Code, "comment not found", err)
|
return nil, errs.New(errs.ErrNotFound.Code, "comment not found", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
commentDto := dto.CommentDto{
|
currentUserID := uint(0)
|
||||||
ID: comment.ID,
|
if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok {
|
||||||
TargetID: comment.TargetID,
|
currentUserID = currentUser.ID
|
||||||
TargetType: comment.TargetType,
|
}
|
||||||
Content: comment.Content,
|
if comment.IsPrivate && currentUserID != comment.UserID {
|
||||||
ReplyID: comment.ReplyID,
|
return nil, errs.ErrForbidden
|
||||||
Depth: comment.Depth,
|
}
|
||||||
CreatedAt: comment.CreatedAt.String(),
|
isLiked := false
|
||||||
UpdatedAt: comment.UpdatedAt.String(),
|
if currentUserID != 0 {
|
||||||
User: comment.User.ToDto(),
|
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
|
||||||
}
|
}
|
||||||
// TODO: 返回更多字段
|
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) {
|
func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommentListReq) ([]dto.CommentDto, error) {
|
||||||
currentUserID := uint(0)
|
currentUserID := uint(0)
|
||||||
if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok {
|
if currentUser, ok := ctxutils.GetCurrentUser(ctx); ok {
|
||||||
currentUserID = currentUser.ID
|
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)
|
||||||
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 {
|
||||||
if err != nil {
|
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list comments", err)
|
||||||
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list comments", err)
|
}
|
||||||
}
|
commentDtos := make([]dto.CommentDto, 0)
|
||||||
|
for _, comment := range comments {
|
||||||
commentDtos := make([]dto.CommentDto, 0)
|
//replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
|
||||||
|
isLiked := false
|
||||||
for _, comment := range comments {
|
if currentUserID != 0 {
|
||||||
//replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
|
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
|
||||||
isLiked := false
|
}
|
||||||
if currentUserID != 0 {
|
ua := utils.ParseUA(comment.UserAgent)
|
||||||
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
|
commentDto := dto.CommentDto{
|
||||||
}
|
ID: comment.ID,
|
||||||
ua := utils.ParseUA(comment.UserAgent)
|
Content: comment.Content,
|
||||||
commentDto := dto.CommentDto{
|
TargetID: comment.TargetID,
|
||||||
ID: comment.ID,
|
TargetType: comment.TargetType,
|
||||||
Content: comment.Content,
|
ReplyID: comment.ReplyID,
|
||||||
TargetID: comment.TargetID,
|
CreatedAt: comment.CreatedAt.String(),
|
||||||
TargetType: comment.TargetType,
|
UpdatedAt: comment.UpdatedAt.String(),
|
||||||
ReplyID: comment.ReplyID,
|
Depth: comment.Depth,
|
||||||
CreatedAt: comment.CreatedAt.String(),
|
User: comment.User.ToDto(),
|
||||||
UpdatedAt: comment.UpdatedAt.String(),
|
ReplyCount: comment.CommentCount,
|
||||||
Depth: comment.Depth,
|
LikeCount: comment.LikeCount,
|
||||||
User: comment.User.ToDto(),
|
IsLiked: isLiked,
|
||||||
ReplyCount: comment.CommentCount,
|
IsPrivate: comment.IsPrivate,
|
||||||
LikeCount: comment.LikeCount,
|
OS: ua.OS + " " + ua.OSVersion,
|
||||||
IsLiked: isLiked,
|
Browser: ua.Browser + " " + ua.BrowserVer,
|
||||||
IsPrivate: comment.IsPrivate,
|
Location: comment.Location,
|
||||||
OS: ua.OS + " " + ua.OSVersion,
|
}
|
||||||
Browser: ua.Browser + " " + ua.BrowserVer,
|
if !comment.ShowClientInfo {
|
||||||
Location: comment.Location,
|
commentDto.Location = ""
|
||||||
}
|
commentDto.OS = ""
|
||||||
commentDtos = append(commentDtos, commentDto)
|
commentDto.Browser = ""
|
||||||
}
|
}
|
||||||
return commentDtos, nil
|
commentDtos = append(commentDtos, commentDto)
|
||||||
|
}
|
||||||
|
return commentDtos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *CommentService) checkTargetExists(targetID uint, targetType string) (bool, error) {
|
func (cs *CommentService) checkTargetExists(targetID uint, targetType string) (bool, error) {
|
||||||
switch targetType {
|
switch targetType {
|
||||||
case constant.TargetTypePost:
|
case constant.TargetTypePost:
|
||||||
if _, err := repo.Post.GetPostByID(strconv.Itoa(int(targetID))); err != nil {
|
if _, err := repo.Post.GetPostByID(strconv.Itoa(int(targetID))); err != nil {
|
||||||
return false, errs.New(errs.ErrNotFound.Code, "post not found", err)
|
return false, errs.New(errs.ErrNotFound.Code, "post not found", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return false, errs.New(errs.ErrBadRequest.Code, "invalid target type", nil)
|
return false, errs.New(errs.ErrBadRequest.Code, "invalid target type", nil)
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type oidcUtils struct{}
|
type oidcUtils struct{}
|
||||||
@ -10,59 +10,59 @@ var Oidc = oidcUtils{}
|
|||||||
|
|
||||||
// RequestToken 请求访问令牌
|
// RequestToken 请求访问令牌
|
||||||
func (u *oidcUtils) RequestToken(tokenEndpoint, clientID, clientSecret, code, redirectURI string) (*TokenResponse, error) {
|
func (u *oidcUtils) RequestToken(tokenEndpoint, clientID, clientSecret, code, redirectURI string) (*TokenResponse, error) {
|
||||||
tokenResp, err := client.R().
|
tokenResp, err := client.R().
|
||||||
SetFormData(map[string]string{
|
SetFormData(map[string]string{
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"client_id": clientID,
|
"client_id": clientID,
|
||||||
"client_secret": clientSecret,
|
"client_secret": clientSecret,
|
||||||
"code": code,
|
"code": code,
|
||||||
"redirect_uri": redirectURI,
|
"redirect_uri": redirectURI,
|
||||||
}).
|
}).
|
||||||
SetHeader("Accept", "application/json").
|
SetHeader("Accept", "application/json").
|
||||||
SetResult(&TokenResponse{}).
|
SetResult(&TokenResponse{}).
|
||||||
Post(tokenEndpoint)
|
Post(tokenEndpoint)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenResp.StatusCode() != 200 {
|
if tokenResp.StatusCode() != 200 {
|
||||||
return nil, fmt.Errorf("状态码: %d,响应: %s", tokenResp.StatusCode(), tokenResp.String())
|
return nil, fmt.Errorf("状态码: %d,响应: %s", tokenResp.StatusCode(), tokenResp.String())
|
||||||
}
|
}
|
||||||
return tokenResp.Result().(*TokenResponse), nil
|
return tokenResp.Result().(*TokenResponse), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestUserInfo 请求用户信息
|
// RequestUserInfo 请求用户信息
|
||||||
func (u *oidcUtils) RequestUserInfo(userInfoEndpoint, accessToken string) (*UserInfo, error) {
|
func (u *oidcUtils) RequestUserInfo(userInfoEndpoint, accessToken string) (*UserInfo, error) {
|
||||||
userInfoResp, err := client.R().
|
userInfoResp, err := client.R().
|
||||||
SetHeader("Authorization", "Bearer "+accessToken).
|
SetHeader("Authorization", "Bearer "+accessToken).
|
||||||
SetHeader("Accept", "application/json").
|
SetHeader("Accept", "application/json").
|
||||||
SetResult(&UserInfo{}).
|
SetResult(&UserInfo{}).
|
||||||
Get(userInfoEndpoint)
|
Get(userInfoEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if userInfoResp.StatusCode() != 200 {
|
if userInfoResp.StatusCode() != 200 {
|
||||||
return nil, fmt.Errorf("状态码: %d,响应: %s", userInfoResp.StatusCode(), userInfoResp.String())
|
return nil, fmt.Errorf("状态码: %d,响应: %s", userInfoResp.StatusCode(), userInfoResp.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return userInfoResp.Result().(*UserInfo), nil
|
return userInfoResp.Result().(*UserInfo), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
IDToken string `json:"id_token,omitempty"`
|
IDToken string `json:"id_token,omitempty"`
|
||||||
RefreshToken string `json:"refresh_token,omitempty"`
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserInfo 定义用户信息结构
|
// UserInfo 定义用户信息结构
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
Sub string `json:"sub"`
|
Sub string `json:"sub"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Picture string `json:"picture,omitempty"`
|
Picture string `json:"picture,omitempty"`
|
||||||
Groups []string `json:"groups,omitempty"` // 可选字段,OIDC提供的用户组信息
|
Groups []string `json:"groups,omitempty"` // 可选字段,OIDC提供的用户组信息
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,15 @@ export async function createComment(
|
|||||||
targetId,
|
targetId,
|
||||||
content,
|
content,
|
||||||
replyId = null,
|
replyId = null,
|
||||||
isPrivate = false
|
isPrivate = false,
|
||||||
|
showClientInfo = true,
|
||||||
}: {
|
}: {
|
||||||
targetType: TargetType
|
targetType: TargetType
|
||||||
targetId: number
|
targetId: number
|
||||||
content: string
|
content: string
|
||||||
replyId: number | null
|
replyId: number | null
|
||||||
isPrivate: boolean
|
isPrivate: boolean
|
||||||
|
showClientInfo: boolean
|
||||||
}
|
}
|
||||||
): Promise<BaseResponse<{ id: number }>> {
|
): Promise<BaseResponse<{ id: number }>> {
|
||||||
const res = await axiosClient.post<BaseResponse<{ id: number }>>('/comment/c', {
|
const res = await axiosClient.post<BaseResponse<{ id: number }>>('/comment/c', {
|
||||||
@ -26,7 +28,8 @@ export async function createComment(
|
|||||||
targetId,
|
targetId,
|
||||||
content,
|
content,
|
||||||
replyId,
|
replyId,
|
||||||
isPrivate
|
isPrivate,
|
||||||
|
showClientInfo,
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { User } from "@/models/user";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import GravatarAvatar, { getGravatarByUser } from "@/components/common/gravatar";
|
import GravatarAvatar from "@/components/common/gravatar";
|
||||||
import { CircleUser } from "lucide-react";
|
import { CircleUser } from "lucide-react";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
@ -20,7 +20,7 @@ export function CommentInput(
|
|||||||
isUpdate = false
|
isUpdate = false
|
||||||
}: {
|
}: {
|
||||||
user: User | null,
|
user: User | null,
|
||||||
onCommentSubmitted: ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => void,
|
onCommentSubmitted: ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => void,
|
||||||
initContent?: string,
|
initContent?: string,
|
||||||
initIsPrivate?: boolean,
|
initIsPrivate?: boolean,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
@ -33,6 +33,7 @@ export function CommentInput(
|
|||||||
const clickToUserProfile = useToUserProfile();
|
const clickToUserProfile = useToUserProfile();
|
||||||
|
|
||||||
const [isPrivate, setIsPrivate] = useState(initIsPrivate);
|
const [isPrivate, setIsPrivate] = useState(initIsPrivate);
|
||||||
|
const [showClientInfo, setShowClientInfo] = useState(true);
|
||||||
const [commentContent, setCommentContent] = useState(initContent);
|
const [commentContent, setCommentContent] = useState(initContent);
|
||||||
|
|
||||||
const handleCommentSubmit = async () => {
|
const handleCommentSubmit = async () => {
|
||||||
@ -54,7 +55,7 @@ export function CommentInput(
|
|||||||
toast.warning(t("comment_unchanged"));
|
toast.warning(t("comment_unchanged"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onCommentSubmitted({ commentContent, isPrivate });
|
onCommentSubmitted({ commentContent, isPrivate, showClientInfo });
|
||||||
setCommentContent("");
|
setCommentContent("");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ export function CommentInput(
|
|||||||
<div className="fade-in-up">
|
<div className="fade-in-up">
|
||||||
<div className="flex py-4 fade-in">
|
<div className="flex py-4 fade-in">
|
||||||
<div onClick={user ? () => clickToUserProfile(user.username) : clickToLogin} className="cursor-pointer flex-shrink-0 w-10 h-10 fade-in">
|
<div onClick={user ? () => clickToUserProfile(user.username) : clickToLogin} className="cursor-pointer flex-shrink-0 w-10 h-10 fade-in">
|
||||||
{user && <GravatarAvatar className="w-full h-full" url={user.avatarUrl} email={user.email} size={100}/>}
|
{user && <GravatarAvatar className="w-full h-full" url={user.avatarUrl} email={user.email} size={100} />}
|
||||||
{!user && <CircleUser className="w-full h-full fade-in" />}
|
{!user && <CircleUser className="w-full h-full fade-in" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 pl-2 fade-in-up">
|
<div className="flex-1 pl-2 fade-in-up">
|
||||||
@ -75,6 +76,13 @@ export function CommentInput(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end fade-in-up space-x-4 items-center">
|
<div className="flex justify-end fade-in-up space-x-4 items-center">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={showClientInfo}
|
||||||
|
onCheckedChange={checked => setShowClientInfo(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label onClick={() => setShowClientInfo(prev => !prev)}>{t("show_client_info")}</Label>
|
||||||
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isPrivate}
|
checked={isPrivate}
|
||||||
|
@ -123,13 +123,14 @@ export function CommentItem(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReply = ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => {
|
const onReply = ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => {
|
||||||
createComment({
|
createComment({
|
||||||
targetType: comment.targetType,
|
targetType: comment.targetType,
|
||||||
targetId: comment.targetId,
|
targetId: comment.targetId,
|
||||||
content: commentContent,
|
content: commentContent,
|
||||||
replyId: comment.id,
|
replyId: comment.id,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
|
showClientInfo
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
toast.success(t("comment_success"));
|
toast.success(t("comment_success"));
|
||||||
reloadReplies();
|
reloadReplies();
|
||||||
|
@ -61,18 +61,18 @@ export function CommentSection(
|
|||||||
});
|
});
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onCommentSubmitted = ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => {
|
const onCommentSubmitted = ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => {
|
||||||
createComment({
|
createComment({
|
||||||
targetType,
|
targetType,
|
||||||
targetId,
|
targetId,
|
||||||
content: commentContent,
|
content: commentContent,
|
||||||
replyId: null,
|
replyId: null,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
|
showClientInfo
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
toast.success(t("comment_success"));
|
toast.success(t("comment_success"));
|
||||||
setTotalCommentCount(c => c + 1);
|
setTotalCommentCount(c => c + 1);
|
||||||
getComment({ id: res.data.id }).then(response => {
|
getComment({ id: res.data.id }).then(response => {
|
||||||
console.log("New comment fetched:", response.data);
|
|
||||||
setComments(prevComments => [response.data, ...prevComments]);
|
setComments(prevComments => [response.data, ...prevComments]);
|
||||||
});
|
});
|
||||||
setActiveInput(null);
|
setActiveInput(null);
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
"private": "私密评论",
|
"private": "私密评论",
|
||||||
"private_placeholder": "悄悄地说一句...",
|
"private_placeholder": "悄悄地说一句...",
|
||||||
"reply": "回复",
|
"reply": "回复",
|
||||||
|
"show_client_info": "展示客户端信息",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"unlike": "取消点赞",
|
"unlike": "取消点赞",
|
||||||
"unlike_success": "已取消点赞",
|
"unlike_success": "已取消点赞",
|
||||||
|
Reference in New Issue
Block a user