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))) }