backend/pkg/post/usecase/post.go

802 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
)
// PostUseCaseParam 定義 PostUseCase 的初始化參數
type PostUseCaseParam struct {
Post domainRepo.PostRepository
Comment domainRepo.CommentRepository
Like domainRepo.LikeRepository
Tag domainRepo.TagRepository
Category domainRepo.CategoryRepository
Logger errs.Logger
}
// PostUseCase 實作 domain usecase 介面
type PostUseCase struct {
PostUseCaseParam
}
// MustPostUseCase 創建新的 PostUseCase如果失敗會 panic
func MustPostUseCase(param PostUseCaseParam) domainUsecase.PostUseCase {
return &PostUseCase{
PostUseCaseParam: param,
}
}
// CreatePost 創建新貼文
func (uc *PostUseCase) CreatePost(ctx context.Context, req domainUsecase.CreatePostRequest) (*domainUsecase.PostResponse, error) {
// 驗證輸入
if err := uc.validateCreatePostRequest(req); err != nil {
return nil, err
}
// 建立貼文實體
post := &entity.Post{
AuthorUID: req.AuthorUID,
Title: req.Title,
Content: req.Content,
Type: req.Type,
CategoryID: req.CategoryID,
Tags: req.Tags,
Images: req.Images,
VideoURL: req.VideoURL,
LinkURL: req.LinkURL,
Status: req.Status,
}
// 如果狀態未指定,預設為草稿
if post.Status == 0 {
post.Status = post.PostStatusDraft
}
// 插入資料庫
if err := uc.Post.Insert(ctx, post); err != nil {
return nil, uc.handleDBError("Post.Insert", req, err)
}
// 處理標籤(更新標籤的貼文數)
if err := uc.updateTagPostCounts(ctx, req.Tags, true); err != nil {
// 記錄錯誤但不中斷流程
uc.Logger.Error(fmt.Sprintf("failed to update tag post counts: %v", err))
}
// 處理分類(更新分類的貼文數)
if req.CategoryID != nil {
if err := uc.Category.IncrementPostCount(ctx, *req.CategoryID); err != nil {
uc.Logger.Error(fmt.Sprintf("failed to increment category post count: %v", err))
}
}
return uc.mapPostToResponse(post), nil
}
// GetPost 取得貼文
func (uc *PostUseCase) GetPost(ctx context.Context, req domainUsecase.GetPostRequest) (*domainUsecase.PostResponse, error) {
// 驗證輸入
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)
}
// 如果提供了 UserUID增加瀏覽數
if req.UserUID != nil {
if err := uc.Post.IncrementViewCount(ctx, req.PostID); err != nil {
uc.Logger.Error(fmt.Sprintf("failed to increment view count: %v", err))
}
}
return uc.mapPostToResponse(post), nil
}
// UpdatePost 更新貼文
func (uc *PostUseCase) UpdatePost(ctx context.Context, req domainUsecase.UpdatePostRequest) (*domainUsecase.PostResponse, error) {
// 驗證輸入
var zeroUUID gocql.UUID
if req.PostID == zeroUUID {
return nil, errs.InputInvalidRangeError("post_id is required")
}
if req.AuthorUID == "" {
return nil, errs.InputInvalidRangeError("author_uid 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.AuthorUID != req.AuthorUID {
return nil, errs.ResNotFoundError("not authorized to update this post")
}
// 檢查是否可編輯
if !post.IsEditable() {
return nil, errs.ResNotFoundError("post is not editable")
}
// 更新欄位
if req.Title != nil {
post.Title = *req.Title
}
if req.Content != nil {
post.Content = *req.Content
}
if req.Type != nil {
post.Type = *req.Type
}
if req.CategoryID != nil {
// 更新分類計數
if post.CategoryID != nil && *post.CategoryID != *req.CategoryID {
if err := uc.Category.DecrementPostCount(ctx, *post.CategoryID); err != nil {
uc.Logger.Error("failed to decrement category post count", errs.LogField{Key: "error", Val: err.Error()})
}
if err := uc.Category.IncrementPostCount(ctx, *req.CategoryID); err != nil {
uc.Logger.Error(fmt.Sprintf("failed to increment category post count: %v", err))
}
}
post.CategoryID = req.CategoryID
}
if req.Tags != nil {
// 更新標籤計數
oldTags := post.Tags
post.Tags = req.Tags
if err := uc.updateTagPostCountsDiff(ctx, oldTags, req.Tags); err != nil {
uc.Logger.Error(fmt.Sprintf("failed to update tag post counts: %v", err))
}
}
if req.Images != nil {
post.Images = req.Images
}
if req.VideoURL != nil {
post.VideoURL = req.VideoURL
}
if req.LinkURL != nil {
post.LinkURL = req.LinkURL
}
// 更新資料庫
if err := uc.Post.Update(ctx, post); err != nil {
return nil, uc.handleDBError("Post.Update", req, err)
}
return uc.mapPostToResponse(post), nil
}
// DeletePost 刪除貼文(軟刪除)
func (uc *PostUseCase) DeletePost(ctx context.Context, req domainUsecase.DeletePostRequest) 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")
}
// 查詢貼文
post, err := uc.Post.FindOne(ctx, req.PostID)
if err != nil {
if repository.IsNotFound(err) {
return errs.ResNotFoundError(fmt.Sprintf("post not found: %s", req.PostID))
}
return uc.handleDBError("Post.FindOne", req, err)
}
// 驗證權限
if post.AuthorUID != req.AuthorUID {
return errs.ResNotFoundError("not authorized to delete this post")
}
// 刪除貼文
if err := uc.Post.Delete(ctx, req.PostID); err != nil {
return uc.handleDBError("Post.Delete", req, err)
}
// 更新標籤和分類計數
if len(post.Tags) > 0 {
if err := uc.updateTagPostCounts(ctx, post.Tags, false); err != nil {
uc.Logger.Error(fmt.Sprintf("failed to update tag post counts: %v", err))
}
}
if post.CategoryID != nil {
if err := uc.Category.DecrementPostCount(ctx, *post.CategoryID); err != nil {
uc.Logger.Error("failed to decrement category post count", errs.LogField{Key: "error", Val: err.Error()})
}
}
return nil
}
// PublishPost 發布貼文
func (uc *PostUseCase) PublishPost(ctx context.Context, req domainUsecase.PublishPostRequest) (*domainUsecase.PostResponse, error) {
// 驗證輸入
var zeroUUID gocql.UUID
if req.PostID == zeroUUID {
return nil, errs.InputInvalidRangeError("post_id is required")
}
if req.AuthorUID == "" {
return nil, errs.InputInvalidRangeError("author_uid 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.AuthorUID != req.AuthorUID {
return nil, errs.ResNotFoundError("not authorized to publish this post")
}
// 發布貼文
post.Publish()
// 更新資料庫
if err := uc.Post.Update(ctx, post); err != nil {
return nil, uc.handleDBError("Post.Update", req, err)
}
return uc.mapPostToResponse(post), nil
}
// ArchivePost 歸檔貼文
func (uc *PostUseCase) ArchivePost(ctx context.Context, req domainUsecase.ArchivePostRequest) 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")
}
// 查詢貼文
post, err := uc.Post.FindOne(ctx, req.PostID)
if err != nil {
if repository.IsNotFound(err) {
return errs.ResNotFoundError(fmt.Sprintf("post not found: %s", req.PostID))
}
return uc.handleDBError("Post.FindOne", req, err)
}
// 驗證權限
if post.AuthorUID != req.AuthorUID {
return errs.ResNotFoundError("not authorized to archive this post")
}
// 歸檔貼文
post.Archive()
// 更新資料庫
return uc.Post.Update(ctx, post)
}
// ListPosts 列出貼文
func (uc *PostUseCase) ListPosts(ctx context.Context, req domainUsecase.ListPostsRequest) (*domainUsecase.ListPostsResponse, error) {
// 驗證分頁參數
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageIndex <= 0 {
req.PageIndex = 1
}
// 構建查詢參數
params := &domainRepo.PostQueryParams{
CategoryID: req.CategoryID,
Tag: req.Tag,
Status: req.Status,
Type: req.Type,
AuthorUID: req.AuthorUID,
CreateStartTime: req.CreateStartTime,
CreateEndTime: req.CreateEndTime,
PageSize: req.PageSize,
PageIndex: req.PageIndex,
OrderBy: req.OrderBy,
OrderDirection: req.OrderDirection,
}
// 執行查詢
var posts []*entity.Post
var total int64
var err error
if req.CategoryID != nil {
posts, total, err = uc.Post.FindByCategoryID(ctx, *req.CategoryID, params)
} else if req.Tag != nil {
posts, total, err = uc.Post.FindByTag(ctx, *req.Tag, params)
} else if req.AuthorUID != nil {
posts, total, err = uc.Post.FindByAuthorUID(ctx, *req.AuthorUID, params)
} else if req.Status != nil {
posts, total, err = uc.Post.FindByStatus(ctx, *req.Status, params)
} else {
// 預設查詢所有已發布的貼文
published := post.PostStatusPublished
params.Status = &published
posts, total, err = uc.Post.FindByStatus(ctx, published, params)
}
if err != nil {
return nil, uc.handleDBError("Post.FindBy*", req, err)
}
// 轉換為 Response
responses := make([]domainUsecase.PostResponse, len(posts))
for i, p := range posts {
responses[i] = *uc.mapPostToResponse(p)
}
return &domainUsecase.ListPostsResponse{
Data: responses,
Page: domainUsecase.Pager{
PageIndex: req.PageIndex,
PageSize: req.PageSize,
Total: total,
TotalPage: calculateTotalPages(total, req.PageSize),
},
}, nil
}
// ListPostsByAuthor 根據作者列出貼文
func (uc *PostUseCase) ListPostsByAuthor(ctx context.Context, req domainUsecase.ListPostsByAuthorRequest) (*domainUsecase.ListPostsResponse, 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.PostQueryParams{
Status: req.Status,
PageSize: req.PageSize,
PageIndex: req.PageIndex,
OrderBy: "created_at",
OrderDirection: "DESC",
}
posts, total, err := uc.Post.FindByAuthorUID(ctx, req.AuthorUID, params)
if err != nil {
return nil, uc.handleDBError("Post.FindByAuthorUID", req, err)
}
responses := make([]domainUsecase.PostResponse, len(posts))
for i, p := range posts {
responses[i] = *uc.mapPostToResponse(p)
}
return &domainUsecase.ListPostsResponse{
Data: responses,
Page: domainUsecase.Pager{
PageIndex: req.PageIndex,
PageSize: req.PageSize,
Total: total,
TotalPage: calculateTotalPages(total, req.PageSize),
},
}, nil
}
// ListPostsByCategory 根據分類列出貼文
func (uc *PostUseCase) ListPostsByCategory(ctx context.Context, req domainUsecase.ListPostsByCategoryRequest) (*domainUsecase.ListPostsResponse, error) {
var zeroUUID gocql.UUID
if req.CategoryID == zeroUUID {
return nil, errs.InputInvalidRangeError("category_id is required")
}
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageIndex <= 0 {
req.PageIndex = 1
}
params := &domainRepo.PostQueryParams{
Status: req.Status,
PageSize: req.PageSize,
PageIndex: req.PageIndex,
OrderBy: "created_at",
OrderDirection: "DESC",
}
posts, total, err := uc.Post.FindByCategoryID(ctx, req.CategoryID, params)
if err != nil {
return nil, uc.handleDBError("Post.FindByCategoryID", req, err)
}
responses := make([]domainUsecase.PostResponse, len(posts))
for i, p := range posts {
responses[i] = *uc.mapPostToResponse(p)
}
return &domainUsecase.ListPostsResponse{
Data: responses,
Page: domainUsecase.Pager{
PageIndex: req.PageIndex,
PageSize: req.PageSize,
Total: total,
TotalPage: calculateTotalPages(total, req.PageSize),
},
}, nil
}
// ListPostsByTag 根據標籤列出貼文
func (uc *PostUseCase) ListPostsByTag(ctx context.Context, req domainUsecase.ListPostsByTagRequest) (*domainUsecase.ListPostsResponse, error) {
if req.Tag == "" {
return nil, errs.InputInvalidRangeError("tag is required")
}
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageIndex <= 0 {
req.PageIndex = 1
}
params := &domainRepo.PostQueryParams{
Status: req.Status,
PageSize: req.PageSize,
PageIndex: req.PageIndex,
OrderBy: "created_at",
OrderDirection: "DESC",
}
posts, total, err := uc.Post.FindByTag(ctx, req.Tag, params)
if err != nil {
return nil, uc.handleDBError("Post.FindByTag", req, err)
}
responses := make([]domainUsecase.PostResponse, len(posts))
for i, p := range posts {
responses[i] = *uc.mapPostToResponse(p)
}
return &domainUsecase.ListPostsResponse{
Data: responses,
Page: domainUsecase.Pager{
PageIndex: req.PageIndex,
PageSize: req.PageSize,
Total: total,
TotalPage: calculateTotalPages(total, req.PageSize),
},
}, nil
}
// GetPinnedPosts 取得置頂貼文
func (uc *PostUseCase) GetPinnedPosts(ctx context.Context, req domainUsecase.GetPinnedPostsRequest) (*domainUsecase.ListPostsResponse, error) {
limit := int64(10)
if req.Limit > 0 {
limit = req.Limit
}
posts, err := uc.Post.FindPinnedPosts(ctx, limit)
if err != nil {
return nil, uc.handleDBError("Post.FindPinnedPosts", req, err)
}
responses := make([]domainUsecase.PostResponse, len(posts))
for i, p := range posts {
responses[i] = *uc.mapPostToResponse(p)
}
return &domainUsecase.ListPostsResponse{
Data: responses,
Page: domainUsecase.Pager{
PageIndex: 1,
PageSize: limit,
Total: int64(len(responses)),
TotalPage: 1,
},
}, nil
}
// LikePost 按讚貼文
func (uc *PostUseCase) LikePost(ctx context.Context, req domainUsecase.LikePostRequest) error {
// 驗證輸入
var zeroUUID gocql.UUID
if req.PostID == zeroUUID {
return errs.InputInvalidRangeError("post_id is required")
}
if req.UserUID == "" {
return errs.InputInvalidRangeError("user_uid is required")
}
// 檢查是否已經按讚
existingLike, err := uc.Like.FindByTargetAndUser(ctx, req.PostID, req.UserUID, "post")
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.PostID,
UserUID: req.UserUID,
TargetType: "post",
}
if err := uc.Like.Insert(ctx, like); err != nil {
return uc.handleDBError("Like.Insert", req, err)
}
// 增加貼文的按讚數
if err := uc.Post.IncrementLikeCount(ctx, req.PostID); err != nil {
uc.Logger.Error(fmt.Sprintf("failed to increment like count: %v", err))
}
return nil
}
// UnlikePost 取消按讚
func (uc *PostUseCase) UnlikePost(ctx context.Context, req domainUsecase.UnlikePostRequest) error {
// 驗證輸入
var zeroUUID gocql.UUID
if req.PostID == zeroUUID {
return errs.InputInvalidRangeError("post_id is required")
}
if req.UserUID == "" {
return errs.InputInvalidRangeError("user_uid is required")
}
// 刪除按讚記錄
if err := uc.Like.DeleteByTargetAndUser(ctx, req.PostID, req.UserUID, "post"); err != nil {
if repository.IsNotFound(err) {
// 已經取消按讚,直接返回成功
return nil
}
return uc.handleDBError("Like.DeleteByTargetAndUser", req, err)
}
// 減少貼文的按讚數
if err := uc.Post.DecrementLikeCount(ctx, req.PostID); err != nil {
uc.Logger.Error(fmt.Sprintf("failed to decrement like count: %v", err))
}
return nil
}
// ViewPost 瀏覽貼文(增加瀏覽數)
func (uc *PostUseCase) ViewPost(ctx context.Context, req domainUsecase.ViewPostRequest) error {
// 驗證輸入
var zeroUUID gocql.UUID
if req.PostID == zeroUUID {
return errs.InputInvalidRangeError("post_id is required")
}
// 增加瀏覽數
if err := uc.Post.IncrementViewCount(ctx, req.PostID); err != nil {
return uc.handleDBError("Post.IncrementViewCount", req, err)
}
return nil
}
// PinPost 置頂貼文
func (uc *PostUseCase) PinPost(ctx context.Context, req domainUsecase.PinPostRequest) 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")
}
// 查詢貼文
post, err := uc.Post.FindOne(ctx, req.PostID)
if err != nil {
if repository.IsNotFound(err) {
return errs.ResNotFoundError(fmt.Sprintf("post not found: %s", req.PostID))
}
return uc.handleDBError("Post.FindOne", req, err)
}
// 驗證權限
if post.AuthorUID != req.AuthorUID {
return errs.ResNotFoundError("not authorized to pin this post")
}
// 置頂貼文
return uc.Post.PinPost(ctx, req.PostID)
}
// UnpinPost 取消置頂
func (uc *PostUseCase) UnpinPost(ctx context.Context, req domainUsecase.UnpinPostRequest) 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")
}
// 查詢貼文
post, err := uc.Post.FindOne(ctx, req.PostID)
if err != nil {
if repository.IsNotFound(err) {
return errs.ResNotFoundError(fmt.Sprintf("post not found: %s", req.PostID))
}
return uc.handleDBError("Post.FindOne", req, err)
}
// 驗證權限
if post.AuthorUID != req.AuthorUID {
return errs.ResNotFoundError("not authorized to unpin this post")
}
// 取消置頂
return uc.Post.UnpinPost(ctx, req.PostID)
}
// validateCreatePostRequest 驗證建立貼文請求
func (uc *PostUseCase) validateCreatePostRequest(req domainUsecase.CreatePostRequest) error {
if req.AuthorUID == "" {
return errs.InputInvalidRangeError("author_uid is required")
}
if req.Title == "" {
return errs.InputInvalidRangeError("title is required")
}
if req.Content == "" {
return errs.InputInvalidRangeError("content is required")
}
if !req.Type.IsValid() {
return errs.InputInvalidRangeError("invalid post type")
}
return nil
}
// mapPostToResponse 將 Post 實體轉換為 PostResponse
func (uc *PostUseCase) mapPostToResponse(post *entity.Post) *domainUsecase.PostResponse {
return &domainUsecase.PostResponse{
ID: post.ID,
AuthorUID: post.AuthorUID,
Title: post.Title,
Content: post.Content,
Type: post.Type,
Status: post.Status,
CategoryID: post.CategoryID,
Tags: post.Tags,
Images: post.Images,
VideoURL: post.VideoURL,
LinkURL: post.LinkURL,
LikeCount: post.LikeCount,
CommentCount: post.CommentCount,
ViewCount: post.ViewCount,
IsPinned: post.IsPinned,
PinnedAt: post.PinnedAt,
PublishedAt: post.PublishedAt,
CreatedAt: post.CreatedAt,
UpdatedAt: post.UpdatedAt,
}
}
// handleDBError 處理資料庫錯誤
func (uc *PostUseCase) 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)
}
// updateTagPostCounts 更新標籤的貼文數
func (uc *PostUseCase) updateTagPostCounts(ctx context.Context, tags []string, increment bool) error {
if len(tags) == 0 {
return nil
}
// 查詢或建立標籤
for _, tagName := range tags {
tag, err := uc.Tag.FindByName(ctx, tagName)
if err != nil {
if repository.IsNotFound(err) {
// 建立新標籤
newTag := &entity.Tag{
Name: tagName,
}
if err := uc.Tag.Insert(ctx, newTag); err != nil {
return fmt.Errorf("failed to create tag: %w", err)
}
tag = newTag
} else {
return fmt.Errorf("failed to find tag: %w", err)
}
}
// 更新計數
if increment {
if err := uc.Tag.IncrementPostCount(ctx, tag.ID); err != nil {
return fmt.Errorf("failed to increment tag count: %w", err)
}
} else {
if err := uc.Tag.DecrementPostCount(ctx, tag.ID); err != nil {
return fmt.Errorf("failed to decrement tag count: %w", err)
}
}
}
return nil
}
// updateTagPostCountsDiff 更新標籤計數(處理差異)
func (uc *PostUseCase) updateTagPostCountsDiff(ctx context.Context, oldTags, newTags []string) error {
// 找出新增和刪除的標籤
oldTagMap := make(map[string]bool)
for _, tag := range oldTags {
oldTagMap[tag] = true
}
newTagMap := make(map[string]bool)
for _, tag := range newTags {
newTagMap[tag] = true
}
// 新增的標籤
for _, tag := range newTags {
if !oldTagMap[tag] {
if err := uc.updateTagPostCounts(ctx, []string{tag}, true); err != nil {
return err
}
}
}
// 刪除的標籤
for _, tag := range oldTags {
if !newTagMap[tag] {
if err := uc.updateTagPostCounts(ctx, []string{tag}, false); err != nil {
return err
}
}
}
return nil
}
// calculateTotalPages 計算總頁數
func calculateTotalPages(total, pageSize int64) int64 {
if pageSize <= 0 {
return 0
}
return int64(math.Ceil(float64(total) / float64(pageSize)))
}