backend/pkg/post/usecase/post.go

802 lines
21 KiB
Go
Raw Normal View History

2025-11-19 09:06:44 +00:00
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)))
}