456 lines
13 KiB
Go
456 lines
13 KiB
Go
|
|
package usecase
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
|
|||
|
|
errs "backend/pkg/library/errors"
|
|||
|
|
"backend/pkg/post/domain/entity"
|
|||
|
|
"backend/pkg/post/domain/post"
|
|||
|
|
domainRepo "backend/pkg/post/domain/repository"
|
|||
|
|
domainUsecase "backend/pkg/post/domain/usecase"
|
|||
|
|
"backend/pkg/post/repository"
|
|||
|
|
|
|||
|
|
"github.com/gocql/gocql"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// CommentUseCaseParam 定義 CommentUseCase 的初始化參數
|
|||
|
|
type CommentUseCaseParam struct {
|
|||
|
|
Comment domainRepo.CommentRepository
|
|||
|
|
Post domainRepo.PostRepository
|
|||
|
|
Like domainRepo.LikeRepository
|
|||
|
|
Logger errs.Logger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CommentUseCase 實作 domain usecase 介面
|
|||
|
|
type CommentUseCase struct {
|
|||
|
|
CommentUseCaseParam
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MustCommentUseCase 創建新的 CommentUseCase(如果失敗會 panic)
|
|||
|
|
func MustCommentUseCase(param CommentUseCaseParam) domainUsecase.CommentUseCase {
|
|||
|
|
return &CommentUseCase{
|
|||
|
|
CommentUseCaseParam: param,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CreateComment 創建新評論
|
|||
|
|
func (uc *CommentUseCase) CreateComment(ctx context.Context, req domainUsecase.CreateCommentRequest) (*domainUsecase.CommentResponse, error) {
|
|||
|
|
// 驗證輸入
|
|||
|
|
if err := uc.validateCreateCommentRequest(req); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 驗證貼文存在
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.PostID == zeroUUID {
|
|||
|
|
return nil, errs.InputInvalidRangeError("post_id is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
post, err := uc.Post.FindOne(ctx, req.PostID)
|
|||
|
|
if err != nil {
|
|||
|
|
if repository.IsNotFound(err) {
|
|||
|
|
return nil, errs.ResNotFoundError(fmt.Sprintf("post not found: %s", req.PostID))
|
|||
|
|
}
|
|||
|
|
return nil, uc.handleDBError("Post.FindOne", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 檢查貼文是否可見
|
|||
|
|
if !post.IsVisible() {
|
|||
|
|
return nil, errs.ResNotFoundError("cannot comment on non-visible post")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 建立評論實體
|
|||
|
|
comment := &entity.Comment{
|
|||
|
|
PostID: req.PostID,
|
|||
|
|
AuthorUID: req.AuthorUID,
|
|||
|
|
ParentID: req.ParentID,
|
|||
|
|
Content: req.Content,
|
|||
|
|
Status: post.CommentStatusPublished,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 插入資料庫
|
|||
|
|
if err := uc.Comment.Insert(ctx, comment); err != nil {
|
|||
|
|
return nil, uc.handleDBError("Comment.Insert", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是回覆,增加父評論的回覆數
|
|||
|
|
if req.ParentID != nil {
|
|||
|
|
if err := uc.Comment.IncrementReplyCount(ctx, *req.ParentID); err != nil {
|
|||
|
|
uc.Logger.Error(fmt.Sprintf("failed to increment reply count: %v", err))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 增加貼文的評論數
|
|||
|
|
if err := uc.Post.IncrementCommentCount(ctx, req.PostID); err != nil {
|
|||
|
|
uc.Logger.Error(fmt.Sprintf("failed to increment comment count: %v", err))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return uc.mapCommentToResponse(comment), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetComment 取得評論
|
|||
|
|
func (uc *CommentUseCase) GetComment(ctx context.Context, req domainUsecase.GetCommentRequest) (*domainUsecase.CommentResponse, error) {
|
|||
|
|
// 驗證輸入
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.CommentID == zeroUUID {
|
|||
|
|
return nil, errs.InputInvalidRangeError("comment_id is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查詢評論
|
|||
|
|
comment, err := uc.Comment.FindOne(ctx, req.CommentID)
|
|||
|
|
if err != nil {
|
|||
|
|
if repository.IsNotFound(err) {
|
|||
|
|
return nil, errs.ResNotFoundError(fmt.Sprintf("comment not found: %s", req.CommentID))
|
|||
|
|
}
|
|||
|
|
return nil, uc.handleDBError("Comment.FindOne", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return uc.mapCommentToResponse(comment), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UpdateComment 更新評論
|
|||
|
|
func (uc *CommentUseCase) UpdateComment(ctx context.Context, req domainUsecase.UpdateCommentRequest) (*domainUsecase.CommentResponse, error) {
|
|||
|
|
// 驗證輸入
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.CommentID == zeroUUID {
|
|||
|
|
return nil, errs.InputInvalidRangeError("comment_id is required")
|
|||
|
|
}
|
|||
|
|
if req.AuthorUID == "" {
|
|||
|
|
return nil, errs.InputInvalidRangeError("author_uid is required")
|
|||
|
|
}
|
|||
|
|
if req.Content == "" {
|
|||
|
|
return nil, errs.InputInvalidRangeError("content is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查詢現有評論
|
|||
|
|
comment, err := uc.Comment.FindOne(ctx, req.CommentID)
|
|||
|
|
if err != nil {
|
|||
|
|
if repository.IsNotFound(err) {
|
|||
|
|
return nil, errs.ResNotFoundError(fmt.Sprintf("comment not found: %s", req.CommentID))
|
|||
|
|
}
|
|||
|
|
return nil, uc.handleDBError("Comment.FindOne", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 驗證權限
|
|||
|
|
if comment.AuthorUID != req.AuthorUID {
|
|||
|
|
return nil, errs.ResNotFoundError("not authorized to update this comment")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 檢查是否可見
|
|||
|
|
if !comment.IsVisible() {
|
|||
|
|
return nil, errs.ResNotFoundError("comment is not visible")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新內容
|
|||
|
|
comment.Content = req.Content
|
|||
|
|
|
|||
|
|
// 更新資料庫
|
|||
|
|
if err := uc.Comment.Update(ctx, comment); err != nil {
|
|||
|
|
return nil, uc.handleDBError("Comment.Update", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return uc.mapCommentToResponse(comment), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DeleteComment 刪除評論(軟刪除)
|
|||
|
|
func (uc *CommentUseCase) DeleteComment(ctx context.Context, req domainUsecase.DeleteCommentRequest) error {
|
|||
|
|
// 驗證輸入
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.CommentID == zeroUUID {
|
|||
|
|
return errs.InputInvalidRangeError("comment_id is required")
|
|||
|
|
}
|
|||
|
|
if req.AuthorUID == "" {
|
|||
|
|
return errs.InputInvalidRangeError("author_uid is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查詢評論
|
|||
|
|
comment, err := uc.Comment.FindOne(ctx, req.CommentID)
|
|||
|
|
if err != nil {
|
|||
|
|
if repository.IsNotFound(err) {
|
|||
|
|
return errs.ResNotFoundError(fmt.Sprintf("comment not found: %s", req.CommentID))
|
|||
|
|
}
|
|||
|
|
return uc.handleDBError("Comment.FindOne", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 驗證權限
|
|||
|
|
if comment.AuthorUID != req.AuthorUID {
|
|||
|
|
return errs.ResNotFoundError("not authorized to delete this comment")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 刪除評論
|
|||
|
|
if err := uc.Comment.Delete(ctx, req.CommentID); err != nil {
|
|||
|
|
return uc.handleDBError("Comment.Delete", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是回覆,減少父評論的回覆數
|
|||
|
|
if comment.ParentID != nil {
|
|||
|
|
if err := uc.Comment.DecrementReplyCount(ctx, *comment.ParentID); err != nil {
|
|||
|
|
uc.Logger.Error(fmt.Sprintf("failed to decrement reply count: %v", err))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 減少貼文的評論數
|
|||
|
|
if err := uc.Post.DecrementCommentCount(ctx, comment.PostID); err != nil {
|
|||
|
|
uc.Logger.Error(fmt.Sprintf("failed to decrement comment count: %v", err))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListComments 列出評論
|
|||
|
|
func (uc *CommentUseCase) ListComments(ctx context.Context, req domainUsecase.ListCommentsRequest) (*domainUsecase.ListCommentsResponse, error) {
|
|||
|
|
// 驗證輸入
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.PostID == zeroUUID {
|
|||
|
|
return nil, errs.InputInvalidRangeError("post_id is required")
|
|||
|
|
}
|
|||
|
|
if req.PageSize <= 0 {
|
|||
|
|
req.PageSize = 20
|
|||
|
|
}
|
|||
|
|
if req.PageIndex <= 0 {
|
|||
|
|
req.PageIndex = 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 構建查詢參數
|
|||
|
|
params := &domainRepo.CommentQueryParams{
|
|||
|
|
PostID: &req.PostID,
|
|||
|
|
ParentID: req.ParentID,
|
|||
|
|
PageSize: req.PageSize,
|
|||
|
|
PageIndex: req.PageIndex,
|
|||
|
|
OrderBy: req.OrderBy,
|
|||
|
|
OrderDirection: req.OrderDirection,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果 OrderBy 未指定,預設為 created_at
|
|||
|
|
if params.OrderBy == "" {
|
|||
|
|
params.OrderBy = "created_at"
|
|||
|
|
}
|
|||
|
|
// 如果 OrderDirection 未指定,預設為 ASC
|
|||
|
|
if params.OrderDirection == "" {
|
|||
|
|
params.OrderDirection = "ASC"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 執行查詢
|
|||
|
|
comments, total, err := uc.Comment.FindByPostID(ctx, req.PostID, params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, uc.handleDBError("Comment.FindByPostID", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 轉換為 Response
|
|||
|
|
responses := make([]domainUsecase.CommentResponse, len(comments))
|
|||
|
|
for i, c := range comments {
|
|||
|
|
responses[i] = *uc.mapCommentToResponse(c)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &domainUsecase.ListCommentsResponse{
|
|||
|
|
Data: responses,
|
|||
|
|
Page: domainUsecase.Pager{
|
|||
|
|
PageIndex: req.PageIndex,
|
|||
|
|
PageSize: req.PageSize,
|
|||
|
|
Total: total,
|
|||
|
|
TotalPage: calculateTotalPages(total, req.PageSize),
|
|||
|
|
},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListReplies 列出回覆
|
|||
|
|
func (uc *CommentUseCase) ListReplies(ctx context.Context, req domainUsecase.ListRepliesRequest) (*domainUsecase.ListCommentsResponse, error) {
|
|||
|
|
// 驗證輸入
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.CommentID == zeroUUID {
|
|||
|
|
return nil, errs.InputInvalidRangeError("comment_id is required")
|
|||
|
|
}
|
|||
|
|
if req.PageSize <= 0 {
|
|||
|
|
req.PageSize = 20
|
|||
|
|
}
|
|||
|
|
if req.PageIndex <= 0 {
|
|||
|
|
req.PageIndex = 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 構建查詢參數
|
|||
|
|
params := &domainRepo.CommentQueryParams{
|
|||
|
|
PageSize: req.PageSize,
|
|||
|
|
PageIndex: req.PageIndex,
|
|||
|
|
OrderBy: "created_at",
|
|||
|
|
OrderDirection: "ASC",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 執行查詢
|
|||
|
|
comments, total, err := uc.Comment.FindReplies(ctx, req.CommentID, params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, uc.handleDBError("Comment.FindReplies", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 轉換為 Response
|
|||
|
|
responses := make([]domainUsecase.CommentResponse, len(comments))
|
|||
|
|
for i, c := range comments {
|
|||
|
|
responses[i] = *uc.mapCommentToResponse(c)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &domainUsecase.ListCommentsResponse{
|
|||
|
|
Data: responses,
|
|||
|
|
Page: domainUsecase.Pager{
|
|||
|
|
PageIndex: req.PageIndex,
|
|||
|
|
PageSize: req.PageSize,
|
|||
|
|
Total: total,
|
|||
|
|
TotalPage: calculateTotalPages(total, req.PageSize),
|
|||
|
|
},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListCommentsByAuthor 根據作者列出評論
|
|||
|
|
func (uc *CommentUseCase) ListCommentsByAuthor(ctx context.Context, req domainUsecase.ListCommentsByAuthorRequest) (*domainUsecase.ListCommentsResponse, error) {
|
|||
|
|
if req.AuthorUID == "" {
|
|||
|
|
return nil, errs.InputInvalidRangeError("author_uid is required")
|
|||
|
|
}
|
|||
|
|
if req.PageSize <= 0 {
|
|||
|
|
req.PageSize = 20
|
|||
|
|
}
|
|||
|
|
if req.PageIndex <= 0 {
|
|||
|
|
req.PageIndex = 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
params := &domainRepo.CommentQueryParams{
|
|||
|
|
PageSize: req.PageSize,
|
|||
|
|
PageIndex: req.PageIndex,
|
|||
|
|
OrderBy: "created_at",
|
|||
|
|
OrderDirection: "DESC",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
comments, total, err := uc.Comment.FindByAuthorUID(ctx, req.AuthorUID, params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, uc.handleDBError("Comment.FindByAuthorUID", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
responses := make([]domainUsecase.CommentResponse, len(comments))
|
|||
|
|
for i, c := range comments {
|
|||
|
|
responses[i] = *uc.mapCommentToResponse(c)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &domainUsecase.ListCommentsResponse{
|
|||
|
|
Data: responses,
|
|||
|
|
Page: domainUsecase.Pager{
|
|||
|
|
PageIndex: req.PageIndex,
|
|||
|
|
PageSize: req.PageSize,
|
|||
|
|
Total: total,
|
|||
|
|
TotalPage: calculateTotalPages(total, req.PageSize),
|
|||
|
|
},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LikeComment 按讚評論
|
|||
|
|
func (uc *CommentUseCase) LikeComment(ctx context.Context, req domainUsecase.LikeCommentRequest) error {
|
|||
|
|
// 驗證輸入
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.CommentID == zeroUUID {
|
|||
|
|
return errs.InputInvalidRangeError("comment_id is required")
|
|||
|
|
}
|
|||
|
|
if req.UserUID == "" {
|
|||
|
|
return errs.InputInvalidRangeError("user_uid is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 檢查是否已經按讚
|
|||
|
|
existingLike, err := uc.Like.FindByTargetAndUser(ctx, req.CommentID, req.UserUID, "comment")
|
|||
|
|
if err == nil && existingLike != nil {
|
|||
|
|
// 已經按讚,直接返回成功
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if err != nil && !repository.IsNotFound(err) {
|
|||
|
|
return uc.handleDBError("Like.FindByTargetAndUser", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 建立按讚記錄
|
|||
|
|
like := &entity.Like{
|
|||
|
|
TargetID: req.CommentID,
|
|||
|
|
UserUID: req.UserUID,
|
|||
|
|
TargetType: "comment",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := uc.Like.Insert(ctx, like); err != nil {
|
|||
|
|
return uc.handleDBError("Like.Insert", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 增加評論的按讚數
|
|||
|
|
if err := uc.Comment.IncrementLikeCount(ctx, req.CommentID); err != nil {
|
|||
|
|
uc.Logger.Error(fmt.Sprintf("failed to increment like count: %v", err))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UnlikeComment 取消按讚評論
|
|||
|
|
func (uc *CommentUseCase) UnlikeComment(ctx context.Context, req domainUsecase.UnlikeCommentRequest) error {
|
|||
|
|
// 驗證輸入
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.CommentID == zeroUUID {
|
|||
|
|
return errs.InputInvalidRangeError("comment_id is required")
|
|||
|
|
}
|
|||
|
|
if req.UserUID == "" {
|
|||
|
|
return errs.InputInvalidRangeError("user_uid is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 刪除按讚記錄
|
|||
|
|
if err := uc.Like.DeleteByTargetAndUser(ctx, req.CommentID, req.UserUID, "comment"); err != nil {
|
|||
|
|
if repository.IsNotFound(err) {
|
|||
|
|
// 已經取消按讚,直接返回成功
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return uc.handleDBError("Like.DeleteByTargetAndUser", req, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 減少評論的按讚數
|
|||
|
|
if err := uc.Comment.DecrementLikeCount(ctx, req.CommentID); err != nil {
|
|||
|
|
uc.Logger.Error(fmt.Sprintf("failed to decrement like count: %v", err))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// validateCreateCommentRequest 驗證建立評論請求
|
|||
|
|
func (uc *CommentUseCase) validateCreateCommentRequest(req domainUsecase.CreateCommentRequest) error {
|
|||
|
|
var zeroUUID gocql.UUID
|
|||
|
|
if req.PostID == zeroUUID {
|
|||
|
|
return errs.InputInvalidRangeError("post_id is required")
|
|||
|
|
}
|
|||
|
|
if req.AuthorUID == "" {
|
|||
|
|
return errs.InputInvalidRangeError("author_uid is required")
|
|||
|
|
}
|
|||
|
|
if req.Content == "" {
|
|||
|
|
return errs.InputInvalidRangeError("content is required")
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// mapCommentToResponse 將 Comment 實體轉換為 CommentResponse
|
|||
|
|
func (uc *CommentUseCase) mapCommentToResponse(comment *entity.Comment) *domainUsecase.CommentResponse {
|
|||
|
|
return &domainUsecase.CommentResponse{
|
|||
|
|
ID: comment.ID,
|
|||
|
|
PostID: comment.PostID,
|
|||
|
|
AuthorUID: comment.AuthorUID,
|
|||
|
|
ParentID: comment.ParentID,
|
|||
|
|
Content: comment.Content,
|
|||
|
|
Status: comment.Status,
|
|||
|
|
LikeCount: comment.LikeCount,
|
|||
|
|
ReplyCount: comment.ReplyCount,
|
|||
|
|
CreatedAt: comment.CreatedAt,
|
|||
|
|
UpdatedAt: comment.UpdatedAt,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// handleDBError 處理資料庫錯誤
|
|||
|
|
func (uc *CommentUseCase) handleDBError(funcName string, req any, err error) error {
|
|||
|
|
return errs.DBErrorErrorL(
|
|||
|
|
uc.Logger,
|
|||
|
|
[]errs.LogField{
|
|||
|
|
{Key: "func", Val: funcName},
|
|||
|
|
{Key: "req", Val: req},
|
|||
|
|
{Key: "error", Val: err.Error()},
|
|||
|
|
},
|
|||
|
|
fmt.Sprintf("database operation failed: %s", funcName),
|
|||
|
|
).Wrap(err)
|
|||
|
|
}
|
|||
|
|
|