802 lines
21 KiB
Go
802 lines
21 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"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 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)))
|
|||
|
|
}
|
|||
|
|
|