From 08d1cf706971302931e5f7eba127bad50dc93777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Mon, 5 Jan 2026 09:58:17 +0800 Subject: [PATCH] fix api server update version --- go.mod | 2 +- go.sum | 2 - pkg/post/domain/const.go | 49 -- pkg/post/domain/entity/category.go | 85 --- pkg/post/domain/entity/comment.go | 114 ---- pkg/post/domain/entity/like.go | 61 -- pkg/post/domain/entity/post.go | 156 ----- pkg/post/domain/entity/tag.go | 60 -- pkg/post/domain/post/comment_status.go | 38 -- pkg/post/domain/post/status.go | 47 -- pkg/post/domain/post/type.go | 39 -- pkg/post/domain/repository/category.go | 26 - pkg/post/domain/repository/comment.go | 46 -- pkg/post/domain/repository/like.go | 37 -- pkg/post/domain/repository/post.go | 54 -- pkg/post/domain/repository/tag.go | 28 - pkg/post/domain/usecase/comment.go | 128 ---- pkg/post/domain/usecase/post.go | 229 ------- pkg/post/repository/category.go | 263 -------- pkg/post/repository/comment.go | 383 ------------ pkg/post/repository/error.go | 34 -- pkg/post/repository/like.go | 228 ------- pkg/post/repository/post.go | 511 ---------------- pkg/post/repository/tag.go | 250 -------- pkg/post/usecase/comment.go | 455 -------------- pkg/post/usecase/post.go | 801 ------------------------- 26 files changed, 1 insertion(+), 4125 deletions(-) delete mode 100644 pkg/post/domain/const.go delete mode 100644 pkg/post/domain/entity/category.go delete mode 100644 pkg/post/domain/entity/comment.go delete mode 100644 pkg/post/domain/entity/like.go delete mode 100644 pkg/post/domain/entity/post.go delete mode 100644 pkg/post/domain/entity/tag.go delete mode 100644 pkg/post/domain/post/comment_status.go delete mode 100644 pkg/post/domain/post/status.go delete mode 100644 pkg/post/domain/post/type.go delete mode 100644 pkg/post/domain/repository/category.go delete mode 100644 pkg/post/domain/repository/comment.go delete mode 100644 pkg/post/domain/repository/like.go delete mode 100644 pkg/post/domain/repository/post.go delete mode 100644 pkg/post/domain/repository/tag.go delete mode 100644 pkg/post/domain/usecase/comment.go delete mode 100644 pkg/post/domain/usecase/post.go delete mode 100644 pkg/post/repository/category.go delete mode 100644 pkg/post/repository/comment.go delete mode 100644 pkg/post/repository/error.go delete mode 100644 pkg/post/repository/like.go delete mode 100644 pkg/post/repository/post.go delete mode 100644 pkg/post/repository/tag.go delete mode 100644 pkg/post/usecase/comment.go delete mode 100644 pkg/post/usecase/post.go diff --git a/go.mod b/go.mod index 6d01990..897e95a 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/matcornic/hermes/v2 v2.1.0 github.com/minchao/go-mitake v1.0.0 github.com/panjf2000/ants/v2 v2.11.3 + github.com/scylladb/gocqlx/v2 v2.8.0 github.com/segmentio/ksuid v1.0.4 github.com/shopspring/decimal v1.4.0 github.com/stretchr/testify v1.11.1 @@ -106,7 +107,6 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect - github.com/scylladb/gocqlx/v2 v2.8.0 // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/go.sum b/go.sum index 8b61775..0bdeaa4 100644 --- a/go.sum +++ b/go.sum @@ -223,8 +223,6 @@ github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cj github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= github.com/scylladb/gocqlx/v2 v2.8.0 h1:f/oIgoEPjKDKd+RIoeHqexsIQVIbalVmT+axwvUqQUg= github.com/scylladb/gocqlx/v2 v2.8.0/go.mod h1:4/+cga34PVqjhgSoo5Nr2fX1MQIqZB5eCE5DK4xeDig= -github.com/scylladb/gocqlx/v3 v3.0.4 h1:37rMVFEUlsGGNYB7OLR7991KwBYR2WA5TU7wtduClas= -github.com/scylladb/gocqlx/v3 v3.0.4/go.mod h1:3vBkGO+HRh/BYypLWXzurQ45u1BAO0VGBhg5VgperPY= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= diff --git a/pkg/post/domain/const.go b/pkg/post/domain/const.go deleted file mode 100644 index 8df4ea4..0000000 --- a/pkg/post/domain/const.go +++ /dev/null @@ -1,49 +0,0 @@ -package domain - -// Business constants for the post service -const ( - // DefaultPageSize is the default page size for pagination - DefaultPageSize = 20 - - // MaxPageSize is the maximum allowed page size - MaxPageSize = 100 - - // MinPageSize is the minimum allowed page size - MinPageSize = 1 - - // MaxPostTitleLength is the maximum length for post title - MaxPostTitleLength = 200 - - // MinPostTitleLength is the minimum length for post title - MinPostTitleLength = 1 - - // MaxPostContentLength is the maximum length for post content - MaxPostContentLength = 10000 - - // MinPostContentLength is the minimum length for post content - MinPostContentLength = 1 - - // MaxCommentLength is the maximum length for comment - MaxCommentLength = 2000 - - // MinCommentLength is the minimum length for comment - MinCommentLength = 1 - - // MaxTagNameLength is the maximum length for tag name - MaxTagNameLength = 50 - - // MinTagNameLength is the minimum length for tag name - MinTagNameLength = 1 - - // MaxTagsPerPost is the maximum number of tags per post - MaxTagsPerPost = 10 - - // DefaultCacheExpiration is the default cache expiration time in seconds - DefaultCacheExpiration = 3600 - - // MaxRetryAttempts is the maximum number of retry attempts for operations - MaxRetryAttempts = 3 - - // DefaultLikeCacheExpiration is the default cache expiration for like counts - DefaultLikeCacheExpiration = 300 // 5 minutes -) diff --git a/pkg/post/domain/entity/category.go b/pkg/post/domain/entity/category.go deleted file mode 100644 index e49e6e5..0000000 --- a/pkg/post/domain/entity/category.go +++ /dev/null @@ -1,85 +0,0 @@ -package entity - -import ( - "errors" - "time" - - "github.com/gocql/gocql" -) - -// Category represents a category entity for organizing posts. -type Category struct { - ID gocql.UUID `db:"id" partition_key:"true"` // Category unique identifier - Slug string `db:"slug"` // URL-friendly slug (unique) - Name string `db:"name"` // Category name - Description *string `db:"description,omitempty"` // Category description (optional) - ParentID *gocql.UUID `db:"parent_id,omitempty"` // Parent category ID (for nested categories) - PostCount int64 `db:"post_count"` // Number of posts in this category - IsActive bool `db:"is_active"` // Whether the category is active - SortOrder int32 `db:"sort_order"` // Sort order for display - CreatedAt int64 `db:"created_at"` // Creation timestamp - UpdatedAt int64 `db:"updated_at"` // Last update timestamp -} - -// TableName returns the Cassandra table name for Category entities. -func (c *Category) TableName() string { - return "categories" -} - -// Validate validates the Category entity -func (c *Category) Validate() error { - if c.Name == "" { - return errors.New("category name is required") - } - if c.Slug == "" { - return errors.New("category slug is required") - } - return nil -} - -// SetTimestamps sets the create and update timestamps -func (c *Category) SetTimestamps() { - now := time.Now().UTC().UnixNano() / 1e6 // milliseconds - if c.CreatedAt == 0 { - c.CreatedAt = now - } - c.UpdatedAt = now -} - -// IsNew returns true if this is a new category (no ID set) -func (c *Category) IsNew() bool { - var zeroUUID gocql.UUID - return c.ID == zeroUUID -} - -// IsRoot returns true if this category has no parent -func (c *Category) IsRoot() bool { - var zeroUUID gocql.UUID - return c.ParentID == nil || *c.ParentID == zeroUUID -} - -// IncrementPostCount increments the post count -func (c *Category) IncrementPostCount() { - c.PostCount++ - c.SetTimestamps() -} - -// DecrementPostCount decrements the post count -func (c *Category) DecrementPostCount() { - if c.PostCount > 0 { - c.PostCount-- - c.SetTimestamps() - } -} - -// Activate activates the category -func (c *Category) Activate() { - c.IsActive = true - c.SetTimestamps() -} - -// Deactivate deactivates the category -func (c *Category) Deactivate() { - c.IsActive = false - c.SetTimestamps() -} diff --git a/pkg/post/domain/entity/comment.go b/pkg/post/domain/entity/comment.go deleted file mode 100644 index c1c469c..0000000 --- a/pkg/post/domain/entity/comment.go +++ /dev/null @@ -1,114 +0,0 @@ -package entity - -import ( - "errors" - "time" - - "backend/pkg/post/domain/post" - - "github.com/gocql/gocql" -) - -// Comment represents a comment entity on a post. -// Comments can be nested (replies to comments). -type Comment struct { - ID gocql.UUID `db:"id" partition_key:"true"` // Comment unique identifier - PostID gocql.UUID `db:"post_id" clustering_key:"true"` // Post ID (clustering key for sorting) - AuthorUID string `db:"author_uid"` // Author user UID - ParentID *gocql.UUID `db:"parent_id,omitempty" clustering_key:"true"` // Parent comment ID (for nested comments) - Content string `db:"content"` // Comment content - Status post.CommentStatus `db:"status"` // Comment status - LikeCount int64 `db:"like_count"` // Number of likes - ReplyCount int64 `db:"reply_count"` // Number of replies - CreatedAt int64 `db:"created_at" clustering_key:"true"` // Creation timestamp (for sorting) - UpdatedAt int64 `db:"updated_at"` // Last update timestamp -} - -// TableName returns the Cassandra table name for Comment entities. -func (c *Comment) TableName() string { - return "comments" -} - -// Validate validates the Comment entity -func (c *Comment) Validate() error { - var zeroUUID gocql.UUID - if c.PostID == zeroUUID { - return errors.New("post_id is required") - } - if c.AuthorUID == "" { - return errors.New("author_uid is required") - } - if len(c.Content) < 1 || len(c.Content) > 2000 { - return errors.New("content length must be between 1 and 2000 characters") - } - if !c.Status.IsValid() { - return errors.New("invalid comment status") - } - return nil -} - -// SetTimestamps sets the create and update timestamps -func (c *Comment) SetTimestamps() { - now := time.Now().UTC().UnixNano() / 1e6 // milliseconds - if c.CreatedAt == 0 { - c.CreatedAt = now - } - c.UpdatedAt = now -} - -// IsNew returns true if this is a new comment (no ID set) -func (c *Comment) IsNew() bool { - var zeroUUID gocql.UUID - return c.ID == zeroUUID -} - -// IsReply returns true if this comment is a reply to another comment -func (c *Comment) IsReply() bool { - var zeroUUID gocql.UUID - return c.ParentID != nil && *c.ParentID != zeroUUID -} - -// Delete marks the comment as deleted (soft delete) -func (c *Comment) Delete() { - c.Status = post.CommentStatusDeleted - c.SetTimestamps() -} - -// Hide hides the comment -func (c *Comment) Hide() { - c.Status = post.CommentStatusHidden - c.SetTimestamps() -} - -// IsVisible returns true if the comment is visible to public -func (c *Comment) IsVisible() bool { - return c.Status.IsVisible() -} - -// IncrementLikeCount increments the like count -func (c *Comment) IncrementLikeCount() { - c.LikeCount++ - c.SetTimestamps() -} - -// DecrementLikeCount decrements the like count -func (c *Comment) DecrementLikeCount() { - if c.LikeCount > 0 { - c.LikeCount-- - c.SetTimestamps() - } -} - -// IncrementReplyCount increments the reply count -func (c *Comment) IncrementReplyCount() { - c.ReplyCount++ - c.SetTimestamps() -} - -// DecrementReplyCount decrements the reply count -func (c *Comment) DecrementReplyCount() { - if c.ReplyCount > 0 { - c.ReplyCount-- - c.SetTimestamps() - } -} diff --git a/pkg/post/domain/entity/like.go b/pkg/post/domain/entity/like.go deleted file mode 100644 index 5a0ec6a..0000000 --- a/pkg/post/domain/entity/like.go +++ /dev/null @@ -1,61 +0,0 @@ -package entity - -import ( - "errors" - "time" - - "github.com/gocql/gocql" -) - -// Like represents a like entity for posts or comments. -// Uses composite primary key: (target_id, user_uid) for uniqueness. -type Like struct { - ID gocql.UUID `db:"id" partition_key:"true"` // Like unique identifier - TargetID gocql.UUID `db:"target_id" clustering_key:"true"` // Target ID (post_id or comment_id) - UserUID string `db:"user_uid" clustering_key:"true"` // User UID who liked - TargetType string `db:"target_type"` // Target type: "post" or "comment" - CreatedAt int64 `db:"created_at"` // Creation timestamp -} - -// TableName returns the Cassandra table name for Like entities. -func (l *Like) TableName() string { - return "likes" -} - -// Validate validates the Like entity -func (l *Like) Validate() error { - var zeroUUID gocql.UUID - if l.TargetID == zeroUUID { - return errors.New("target_id is required") - } - if l.UserUID == "" { - return errors.New("user_uid is required") - } - if l.TargetType != "post" && l.TargetType != "comment" { - return errors.New("target_type must be 'post' or 'comment'") - } - return nil -} - -// SetTimestamps sets the create timestamp -func (l *Like) SetTimestamps() { - if l.CreatedAt == 0 { - l.CreatedAt = time.Now().UTC().UnixNano() / 1e6 // milliseconds - } -} - -// IsNew returns true if this is a new like (no ID set) -func (l *Like) IsNew() bool { - var zeroUUID gocql.UUID - return l.ID == zeroUUID -} - -// IsPostLike returns true if this like is for a post -func (l *Like) IsPostLike() bool { - return l.TargetType == "post" -} - -// IsCommentLike returns true if this like is for a comment -func (l *Like) IsCommentLike() bool { - return l.TargetType == "comment" -} diff --git a/pkg/post/domain/entity/post.go b/pkg/post/domain/entity/post.go deleted file mode 100644 index e8f0a37..0000000 --- a/pkg/post/domain/entity/post.go +++ /dev/null @@ -1,156 +0,0 @@ -package entity - -import ( - "errors" - "time" - - "backend/pkg/post/domain/post" - - "github.com/gocql/gocql" -) - -// Post represents a post entity in the system. -// It contains the main content and metadata for user posts. -type Post struct { - ID gocql.UUID `db:"id" partition_key:"true"` // Post unique identifier - AuthorUID string `db:"author_uid"` // Author user UID - Title string `db:"title"` // Post title - Content string `db:"content"` // Post content - Type post.Type `db:"type"` // Post type (text, image, video, etc.) - Status post.Status `db:"status"` // Post status (draft, published, etc.) - CategoryID *gocql.UUID `db:"category_id,omitempty"` // Category ID (optional) - Tags []string `db:"tags,omitempty"` // Post tags - Images []string `db:"images,omitempty"` // Image URLs (optional) - VideoURL *string `db:"video_url,omitempty"` // Video URL (optional) - LinkURL *string `db:"link_url,omitempty"` // Link URL (optional) - LikeCount int64 `db:"like_count"` // Number of likes - CommentCount int64 `db:"comment_count"` // Number of comments - ViewCount int64 `db:"view_count"` // Number of views - IsPinned bool `db:"is_pinned"` // Whether the post is pinned - PinnedAt *int64 `db:"pinned_at,omitempty"` // Pinned timestamp (optional) - PublishedAt *int64 `db:"published_at,omitempty"` // Published timestamp (optional) - CreatedAt int64 `db:"created_at"` // Creation timestamp - UpdatedAt int64 `db:"updated_at"` // Last update timestamp -} - -// TableName returns the Cassandra table name for Post entities. -func (p *Post) TableName() string { - return "posts" -} - -// Validate validates the Post entity -func (p *Post) Validate() error { - if p.AuthorUID == "" { - return errors.New("author_uid is required") - } - if len(p.Title) < 1 || len(p.Title) > 200 { - return errors.New("title length must be between 1 and 200 characters") - } - if len(p.Content) < 1 || len(p.Content) > 10000 { - return errors.New("content length must be between 1 and 10000 characters") - } - if !p.Type.IsValid() { - return errors.New("invalid post type") - } - if !p.Status.IsValid() { - return errors.New("invalid post status") - } - if len(p.Tags) > 10 { - return errors.New("maximum 10 tags allowed per post") - } - return nil -} - -// SetTimestamps sets the create and update timestamps -func (p *Post) SetTimestamps() { - now := time.Now().UTC().UnixNano() / 1e6 // milliseconds - if p.CreatedAt == 0 { - p.CreatedAt = now - } - p.UpdatedAt = now -} - -// IsNew returns true if this is a new post (no ID set) -func (p *Post) IsNew() bool { - var zeroUUID gocql.UUID - return p.ID == zeroUUID -} - -// Publish marks the post as published -func (p *Post) Publish() { - p.Status = post.PostStatusPublished - now := time.Now().UTC().UnixNano() / 1e6 - p.PublishedAt = &now - p.SetTimestamps() -} - -// Archive marks the post as archived -func (p *Post) Archive() { - p.Status = post.PostStatusArchived - p.SetTimestamps() -} - -// Delete marks the post as deleted (soft delete) -func (p *Post) Delete() { - p.Status = post.PostStatusDeleted - p.SetTimestamps() -} - -// IsVisible returns true if the post is visible to public -func (p *Post) IsVisible() bool { - return p.Status.IsVisible() -} - -// IsEditable returns true if the post can be edited -func (p *Post) IsEditable() bool { - return p.Status.IsEditable() -} - -// IncrementLikeCount increments the like count -func (p *Post) IncrementLikeCount() { - p.LikeCount++ - p.SetTimestamps() -} - -// DecrementLikeCount decrements the like count -func (p *Post) DecrementLikeCount() { - if p.LikeCount > 0 { - p.LikeCount-- - p.SetTimestamps() - } -} - -// IncrementCommentCount increments the comment count -func (p *Post) IncrementCommentCount() { - p.CommentCount++ - p.SetTimestamps() -} - -// DecrementCommentCount decrements the comment count -func (p *Post) DecrementCommentCount() { - if p.CommentCount > 0 { - p.CommentCount-- - p.SetTimestamps() - } -} - -// IncrementViewCount increments the view count -func (p *Post) IncrementViewCount() { - p.ViewCount++ - p.SetTimestamps() -} - -// Pin pins the post -func (p *Post) Pin() { - p.IsPinned = true - now := time.Now().UTC().UnixNano() / 1e6 - p.PinnedAt = &now - p.SetTimestamps() -} - -// Unpin unpins the post -func (p *Post) Unpin() { - p.IsPinned = false - p.PinnedAt = nil - p.SetTimestamps() -} diff --git a/pkg/post/domain/entity/tag.go b/pkg/post/domain/entity/tag.go deleted file mode 100644 index a35f8ac..0000000 --- a/pkg/post/domain/entity/tag.go +++ /dev/null @@ -1,60 +0,0 @@ -package entity - -import ( - "errors" - "time" - - "github.com/gocql/gocql" -) - -// Tag represents a tag entity for categorizing posts. -type Tag struct { - ID gocql.UUID `db:"id" partition_key:"true"` // Tag unique identifier - Name string `db:"name"` // Tag name (unique) - Description *string `db:"description,omitempty"` // Tag description (optional) - PostCount int64 `db:"post_count"` // Number of posts using this tag - CreatedAt int64 `db:"created_at"` // Creation timestamp - UpdatedAt int64 `db:"updated_at"` // Last update timestamp -} - -// TableName returns the Cassandra table name for Tag entities. -func (t *Tag) TableName() string { - return "tags" -} - -// Validate validates the Tag entity -func (t *Tag) Validate() error { - if len(t.Name) < 1 || len(t.Name) > 50 { - return errors.New("tag name length must be between 1 and 50 characters") - } - return nil -} - -// SetTimestamps sets the create and update timestamps -func (t *Tag) SetTimestamps() { - now := time.Now().UTC().UnixNano() / 1e6 // milliseconds - if t.CreatedAt == 0 { - t.CreatedAt = now - } - t.UpdatedAt = now -} - -// IsNew returns true if this is a new tag (no ID set) -func (t *Tag) IsNew() bool { - var zeroUUID gocql.UUID - return t.ID == zeroUUID -} - -// IncrementPostCount increments the post count -func (t *Tag) IncrementPostCount() { - t.PostCount++ - t.SetTimestamps() -} - -// DecrementPostCount decrements the post count -func (t *Tag) DecrementPostCount() { - if t.PostCount > 0 { - t.PostCount-- - t.SetTimestamps() - } -} diff --git a/pkg/post/domain/post/comment_status.go b/pkg/post/domain/post/comment_status.go deleted file mode 100644 index de098af..0000000 --- a/pkg/post/domain/post/comment_status.go +++ /dev/null @@ -1,38 +0,0 @@ -package post - -// CommentStatus 評論狀態 -type CommentStatus int32 - -func (s CommentStatus) CodeToString() string { - result, ok := commentStatusMap[s] - if !ok { - return "" - } - return result -} - -var commentStatusMap = map[CommentStatus]string{ - CommentStatusPublished: "published", // 已發布 - CommentStatusDeleted: "deleted", // 已刪除 - CommentStatusHidden: "hidden", // 隱藏 -} - -func (s CommentStatus) ToInt32() int32 { - return int32(s) -} - -const ( - CommentStatusPublished CommentStatus = 0 // 已發布 - CommentStatusDeleted CommentStatus = 1 // 已刪除 - CommentStatusHidden CommentStatus = 2 // 隱藏 -) - -// IsValid returns true if the status is valid -func (s CommentStatus) IsValid() bool { - return s >= CommentStatusPublished && s <= CommentStatusHidden -} - -// IsVisible returns true if the comment is visible to public -func (s CommentStatus) IsVisible() bool { - return s == CommentStatusPublished -} diff --git a/pkg/post/domain/post/status.go b/pkg/post/domain/post/status.go deleted file mode 100644 index 13c1ee4..0000000 --- a/pkg/post/domain/post/status.go +++ /dev/null @@ -1,47 +0,0 @@ -package post - -// Status 貼文狀態 -type Status int32 - -func (s Status) CodeToString() string { - result, ok := postStatusMap[s] - if !ok { - return "" - } - return result -} - -var postStatusMap = map[Status]string{ - PostStatusDraft: "draft", // 草稿 - PostStatusPublished: "published", // 已發布 - PostStatusArchived: "archived", // 已歸檔 - PostStatusDeleted: "deleted", // 已刪除 - PostStatusHidden: "hidden", // 隱藏 -} - -func (s Status) ToInt32() int32 { - return int32(s) -} - -const ( - PostStatusDraft Status = 0 // 草稿 - PostStatusPublished Status = 1 // 已發布 - PostStatusArchived Status = 2 // 已歸檔 - PostStatusDeleted Status = 3 // 已刪除 - PostStatusHidden Status = 4 // 隱藏 -) - -// IsValid returns true if the status is valid -func (s Status) IsValid() bool { - return s >= PostStatusDraft && s <= PostStatusHidden -} - -// IsVisible returns true if the post is visible to public -func (s Status) IsVisible() bool { - return s == PostStatusPublished -} - -// IsEditable returns true if the post can be edited -func (s Status) IsEditable() bool { - return s == PostStatusDraft || s == PostStatusPublished -} diff --git a/pkg/post/domain/post/type.go b/pkg/post/domain/post/type.go deleted file mode 100644 index 71966ab..0000000 --- a/pkg/post/domain/post/type.go +++ /dev/null @@ -1,39 +0,0 @@ -package post - -// Type 貼文類型 -type Type int32 - -func (t Type) CodeToString() string { - result, ok := postTypeMap[t] - if !ok { - return "" - } - return result -} - -var postTypeMap = map[Type]string{ - TypeText: "text", // 純文字 - TypeImage: "image", // 圖片 - TypeVideo: "video", // 影片 - TypeLink: "link", // 連結 - TypePoll: "poll", // 投票 - TypeArticle: "article", // 長文 -} - -func (t Type) ToInt32() int32 { - return int32(t) -} - -const ( - TypeText Type = 0 // 純文字 - TypeImage Type = 1 // 圖片 - TypeVideo Type = 2 // 影片 - TypeLink Type = 3 // 連結 - TypePoll Type = 4 // 投票 - TypeArticle Type = 5 // 長文 -) - -// IsValid returns true if the type is valid -func (t Type) IsValid() bool { - return t >= TypeText && t <= TypeArticle -} diff --git a/pkg/post/domain/repository/category.go b/pkg/post/domain/repository/category.go deleted file mode 100644 index 56c822b..0000000 --- a/pkg/post/domain/repository/category.go +++ /dev/null @@ -1,26 +0,0 @@ -package repository - -import ( - "context" - - "backend/pkg/post/domain/entity" -) - -// CategoryRepository defines the interface for category data access operations -type CategoryRepository interface { - BaseCategoryRepository - FindBySlug(ctx context.Context, slug string) (*entity.Category, error) - FindByParentID(ctx context.Context, parentID string) ([]*entity.Category, error) - FindRootCategories(ctx context.Context) ([]*entity.Category, error) - FindActive(ctx context.Context) ([]*entity.Category, error) - IncrementPostCount(ctx context.Context, categoryID string) error - DecrementPostCount(ctx context.Context, categoryID string) error -} - -// BaseCategoryRepository defines basic CRUD operations for categories -type BaseCategoryRepository interface { - Insert(ctx context.Context, data *entity.Category) error - FindOne(ctx context.Context, id string) (*entity.Category, error) - Update(ctx context.Context, data *entity.Category) error - Delete(ctx context.Context, id string) error -} diff --git a/pkg/post/domain/repository/comment.go b/pkg/post/domain/repository/comment.go deleted file mode 100644 index f94d7c5..0000000 --- a/pkg/post/domain/repository/comment.go +++ /dev/null @@ -1,46 +0,0 @@ -package repository - -import ( - "context" - - "backend/pkg/post/domain/entity" - "backend/pkg/post/domain/post" - - "github.com/gocql/gocql" -) - -// CommentRepository defines the interface for comment data access operations -type CommentRepository interface { - BaseCommentRepository - FindByPostID(ctx context.Context, postID gocql.UUID, params *CommentQueryParams) ([]*entity.Comment, int64, error) - FindByParentID(ctx context.Context, parentID gocql.UUID, params *CommentQueryParams) ([]*entity.Comment, int64, error) - FindByAuthorUID(ctx context.Context, authorUID string, params *CommentQueryParams) ([]*entity.Comment, int64, error) - FindReplies(ctx context.Context, commentID gocql.UUID, params *CommentQueryParams) ([]*entity.Comment, int64, error) - IncrementLikeCount(ctx context.Context, commentID gocql.UUID) error - DecrementLikeCount(ctx context.Context, commentID gocql.UUID) error - IncrementReplyCount(ctx context.Context, commentID gocql.UUID) error - DecrementReplyCount(ctx context.Context, commentID gocql.UUID) error - UpdateStatus(ctx context.Context, commentID gocql.UUID, status post.CommentStatus) error -} - -// BaseCommentRepository defines basic CRUD operations for comments -type BaseCommentRepository interface { - Insert(ctx context.Context, data *entity.Comment) error - FindOne(ctx context.Context, id gocql.UUID) (*entity.Comment, error) - Update(ctx context.Context, data *entity.Comment) error - Delete(ctx context.Context, id gocql.UUID) error -} - -// CommentQueryParams defines query parameters for comment listing -type CommentQueryParams struct { - PostID *gocql.UUID - ParentID *gocql.UUID - AuthorUID *string - Status *post.CommentStatus - CreateStartTime *int64 - CreateEndTime *int64 - PageSize int64 - PageIndex int64 - OrderBy string // "created_at", "like_count" - OrderDirection string // "ASC", "DESC" -} diff --git a/pkg/post/domain/repository/like.go b/pkg/post/domain/repository/like.go deleted file mode 100644 index 143a16c..0000000 --- a/pkg/post/domain/repository/like.go +++ /dev/null @@ -1,37 +0,0 @@ -package repository - -import ( - "context" - - "backend/pkg/post/domain/entity" - - "github.com/gocql/gocql" -) - -// LikeRepository defines the interface for like data access operations -type LikeRepository interface { - BaseLikeRepository - FindByTargetID(ctx context.Context, targetID gocql.UUID, targetType string) ([]*entity.Like, error) - FindByUserUID(ctx context.Context, userUID string, params *LikeQueryParams) ([]*entity.Like, int64, error) - FindByTargetAndUser(ctx context.Context, targetID gocql.UUID, userUID string, targetType string) (*entity.Like, error) - CountByTargetID(ctx context.Context, targetID gocql.UUID, targetType string) (int64, error) - DeleteByTargetAndUser(ctx context.Context, targetID gocql.UUID, userUID string, targetType string) error -} - -// BaseLikeRepository defines basic CRUD operations for likes -type BaseLikeRepository interface { - Insert(ctx context.Context, data *entity.Like) error - FindOne(ctx context.Context, id gocql.UUID) (*entity.Like, error) - Delete(ctx context.Context, id gocql.UUID) error -} - -// LikeQueryParams defines query parameters for like listing -type LikeQueryParams struct { - TargetID *gocql.UUID - TargetType *string - UserUID *string - PageSize int64 - PageIndex int64 - OrderBy string // "created_at" - OrderDirection string // "ASC", "DESC" -} diff --git a/pkg/post/domain/repository/post.go b/pkg/post/domain/repository/post.go deleted file mode 100644 index 13d4b7f..0000000 --- a/pkg/post/domain/repository/post.go +++ /dev/null @@ -1,54 +0,0 @@ -package repository - -import ( - "context" - - "backend/pkg/post/domain/entity" - "backend/pkg/post/domain/post" - - "github.com/gocql/gocql" -) - -// PostRepository defines the interface for post data access operations -type PostRepository interface { - BasePostRepository - FindByAuthorUID(ctx context.Context, authorUID string, params *PostQueryParams) ([]*entity.Post, int64, error) - FindByCategoryID(ctx context.Context, categoryID gocql.UUID, params *PostQueryParams) ([]*entity.Post, int64, error) - FindByTag(ctx context.Context, tagName string, params *PostQueryParams) ([]*entity.Post, int64, error) - FindPinnedPosts(ctx context.Context, limit int64) ([]*entity.Post, error) - FindByStatus(ctx context.Context, status post.Status, params *PostQueryParams) ([]*entity.Post, int64, error) - IncrementLikeCount(ctx context.Context, postID gocql.UUID) error - DecrementLikeCount(ctx context.Context, postID gocql.UUID) error - IncrementCommentCount(ctx context.Context, postID gocql.UUID) error - DecrementCommentCount(ctx context.Context, postID gocql.UUID) error - IncrementViewCount(ctx context.Context, postID gocql.UUID) error - UpdateStatus(ctx context.Context, postID gocql.UUID, status post.Status) error - PinPost(ctx context.Context, postID gocql.UUID) error - UnpinPost(ctx context.Context, postID gocql.UUID) error -} - -// BasePostRepository defines basic CRUD operations for posts -type BasePostRepository interface { - Insert(ctx context.Context, data *entity.Post) error - FindOne(ctx context.Context, id gocql.UUID) (*entity.Post, error) - Update(ctx context.Context, data *entity.Post) error - Delete(ctx context.Context, id gocql.UUID) error -} - -// PostQueryParams defines query parameters for post listing -type PostQueryParams struct { - AuthorUID *string - CategoryID *gocql.UUID - Tag *string - Status *post.Status - Type *post.Type - IsPinned *bool - CreateStartTime *int64 - CreateEndTime *int64 - PublishedStartTime *int64 - PublishedEndTime *int64 - PageSize int64 - PageIndex int64 - OrderBy string // "created_at", "published_at", "like_count", "view_count" - OrderDirection string // "ASC", "DESC" -} diff --git a/pkg/post/domain/repository/tag.go b/pkg/post/domain/repository/tag.go deleted file mode 100644 index 8f6316d..0000000 --- a/pkg/post/domain/repository/tag.go +++ /dev/null @@ -1,28 +0,0 @@ -package repository - -import ( - "context" - - "backend/pkg/post/domain/entity" - - "github.com/gocql/gocql" -) - -// TagRepository defines the interface for tag data access operations -type TagRepository interface { - BaseTagRepository - FindByName(ctx context.Context, name string) (*entity.Tag, error) - FindByNames(ctx context.Context, names []string) ([]*entity.Tag, error) - FindPopular(ctx context.Context, limit int64) ([]*entity.Tag, error) - IncrementPostCount(ctx context.Context, tagID gocql.UUID) error - DecrementPostCount(ctx context.Context, tagID gocql.UUID) error -} - -// BaseTagRepository defines basic CRUD operations for tags -type BaseTagRepository interface { - Insert(ctx context.Context, data *entity.Tag) error - FindOne(ctx context.Context, id gocql.UUID) (*entity.Tag, error) - Update(ctx context.Context, data *entity.Tag) error - Delete(ctx context.Context, id gocql.UUID) error -} - diff --git a/pkg/post/domain/usecase/comment.go b/pkg/post/domain/usecase/comment.go deleted file mode 100644 index 83132ed..0000000 --- a/pkg/post/domain/usecase/comment.go +++ /dev/null @@ -1,128 +0,0 @@ -package usecase - -import ( - "context" - - "backend/pkg/post/domain/post" - - "github.com/gocql/gocql" -) - -// CommentUseCase defines the interface for comment business logic operations -type CommentUseCase interface { - CommentCRUDUseCase - CommentQueryUseCase - CommentInteractionUseCase -} - -// CommentCRUDUseCase defines CRUD operations for comments -type CommentCRUDUseCase interface { - // CreateComment creates a new comment - CreateComment(ctx context.Context, req CreateCommentRequest) (*CommentResponse, error) - // GetComment retrieves a comment by ID - GetComment(ctx context.Context, req GetCommentRequest) (*CommentResponse, error) - // UpdateComment updates an existing comment - UpdateComment(ctx context.Context, req UpdateCommentRequest) (*CommentResponse, error) - // DeleteComment deletes a comment (soft delete) - DeleteComment(ctx context.Context, req DeleteCommentRequest) error -} - -// CommentQueryUseCase defines query operations for comments -type CommentQueryUseCase interface { - // ListComments lists comments for a post - ListComments(ctx context.Context, req ListCommentsRequest) (*ListCommentsResponse, error) - // ListReplies lists replies to a comment - ListReplies(ctx context.Context, req ListRepliesRequest) (*ListCommentsResponse, error) - // ListCommentsByAuthor lists comments by author - ListCommentsByAuthor(ctx context.Context, req ListCommentsByAuthorRequest) (*ListCommentsResponse, error) -} - -// CommentInteractionUseCase defines interaction operations for comments -type CommentInteractionUseCase interface { - // LikeComment likes a comment - LikeComment(ctx context.Context, req LikeCommentRequest) error - // UnlikeComment unlikes a comment - UnlikeComment(ctx context.Context, req UnlikeCommentRequest) error -} - -// CreateCommentRequest represents a request to create a comment -type CreateCommentRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - AuthorUID string `json:"author_uid"` // Author user UID - ParentID *gocql.UUID `json:"parent_id,omitempty"` // Parent comment ID (optional, for replies) - Content string `json:"content"` // Comment content -} - -// UpdateCommentRequest represents a request to update a comment -type UpdateCommentRequest struct { - CommentID gocql.UUID `json:"comment_id"` // Comment ID - AuthorUID string `json:"author_uid"` // Author user UID (for authorization) - Content string `json:"content"` // Comment content -} - -// GetCommentRequest represents a request to get a comment -type GetCommentRequest struct { - CommentID gocql.UUID `json:"comment_id"` // Comment ID -} - -// DeleteCommentRequest represents a request to delete a comment -type DeleteCommentRequest struct { - CommentID gocql.UUID `json:"comment_id"` // Comment ID - AuthorUID string `json:"author_uid"` // Author user UID (for authorization) -} - -// ListCommentsRequest represents a request to list comments -type ListCommentsRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - ParentID *gocql.UUID `json:"parent_id,omitempty"` // Parent comment ID (optional, for replies only) - PageSize int64 `json:"page_size"` // Page size - PageIndex int64 `json:"page_index"` // Page index - OrderBy string `json:"order_by,omitempty"` // Order by field (default: "created_at") - OrderDirection string `json:"order_direction,omitempty"` // Order direction (ASC/DESC, default: ASC) -} - -// ListRepliesRequest represents a request to list replies to a comment -type ListRepliesRequest struct { - CommentID gocql.UUID `json:"comment_id"` // Comment ID - PageSize int64 `json:"page_size"` // Page size - PageIndex int64 `json:"page_index"` // Page index -} - -// ListCommentsByAuthorRequest represents a request to list comments by author -type ListCommentsByAuthorRequest struct { - AuthorUID string `json:"author_uid"` // Author UID - PageSize int64 `json:"page_size"` // Page size - PageIndex int64 `json:"page_index"` // Page index -} - -// LikeCommentRequest represents a request to like a comment -type LikeCommentRequest struct { - CommentID gocql.UUID `json:"comment_id"` // Comment ID - UserUID string `json:"user_uid"` // User UID -} - -// UnlikeCommentRequest represents a request to unlike a comment -type UnlikeCommentRequest struct { - CommentID gocql.UUID `json:"comment_id"` // Comment ID - UserUID string `json:"user_uid"` // User UID -} - -// CommentResponse represents a comment response -type CommentResponse struct { - ID gocql.UUID `json:"id"` - PostID gocql.UUID `json:"post_id"` - AuthorUID string `json:"author_uid"` - ParentID *gocql.UUID `json:"parent_id,omitempty"` - Content string `json:"content"` - Status post.CommentStatus `json:"status"` - LikeCount int64 `json:"like_count"` - ReplyCount int64 `json:"reply_count"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -// ListCommentsResponse represents a list of comments response -type ListCommentsResponse struct { - Data []CommentResponse `json:"data"` - Page Pager `json:"page"` -} diff --git a/pkg/post/domain/usecase/post.go b/pkg/post/domain/usecase/post.go deleted file mode 100644 index 1a0bc2c..0000000 --- a/pkg/post/domain/usecase/post.go +++ /dev/null @@ -1,229 +0,0 @@ -package usecase - -import ( - "context" - - "backend/pkg/post/domain/post" - - "github.com/gocql/gocql" -) - -// PostUseCase defines the interface for post business logic operations -type PostUseCase interface { - PostCRUDUseCase - PostQueryUseCase - PostInteractionUseCase - PostManagementUseCase -} - -// PostCRUDUseCase defines CRUD operations for posts -type PostCRUDUseCase interface { - // CreatePost creates a new post - CreatePost(ctx context.Context, req CreatePostRequest) (*PostResponse, error) - // GetPost retrieves a post by ID - GetPost(ctx context.Context, req GetPostRequest) (*PostResponse, error) - // UpdatePost updates an existing post - UpdatePost(ctx context.Context, req UpdatePostRequest) (*PostResponse, error) - // DeletePost deletes a post (soft delete) - DeletePost(ctx context.Context, req DeletePostRequest) error - // PublishPost publishes a draft post - PublishPost(ctx context.Context, req PublishPostRequest) (*PostResponse, error) - // ArchivePost archives a post - ArchivePost(ctx context.Context, req ArchivePostRequest) error -} - -// PostQueryUseCase defines query operations for posts -type PostQueryUseCase interface { - // ListPosts lists posts with filters and pagination - ListPosts(ctx context.Context, req ListPostsRequest) (*ListPostsResponse, error) - // ListPostsByAuthor lists posts by author UID - ListPostsByAuthor(ctx context.Context, req ListPostsByAuthorRequest) (*ListPostsResponse, error) - // ListPostsByCategory lists posts by category - ListPostsByCategory(ctx context.Context, req ListPostsByCategoryRequest) (*ListPostsResponse, error) - // ListPostsByTag lists posts by tag - ListPostsByTag(ctx context.Context, req ListPostsByTagRequest) (*ListPostsResponse, error) - // GetPinnedPosts gets pinned posts - GetPinnedPosts(ctx context.Context, req GetPinnedPostsRequest) (*ListPostsResponse, error) -} - -// PostInteractionUseCase defines interaction operations for posts -type PostInteractionUseCase interface { - // LikePost likes a post - LikePost(ctx context.Context, req LikePostRequest) error - // UnlikePost unlikes a post - UnlikePost(ctx context.Context, req UnlikePostRequest) error - // ViewPost increments view count - ViewPost(ctx context.Context, req ViewPostRequest) error -} - -// PostManagementUseCase defines management operations for posts -type PostManagementUseCase interface { - // PinPost pins a post - PinPost(ctx context.Context, req PinPostRequest) error - // UnpinPost unpins a post - UnpinPost(ctx context.Context, req UnpinPostRequest) error -} - -// CreatePostRequest represents a request to create a post -type CreatePostRequest struct { - AuthorUID string `json:"author_uid"` // Author user UID - Title string `json:"title"` // Post title - Content string `json:"content"` // Post content - Type post.Type `json:"type"` // Post type - CategoryID *gocql.UUID `json:"category_id,omitempty"` // Category ID (optional) - Tags []string `json:"tags,omitempty"` // Post tags (optional) - Images []string `json:"images,omitempty"` // Image URLs (optional) - VideoURL *string `json:"video_url,omitempty"` // Video URL (optional) - LinkURL *string `json:"link_url,omitempty"` // Link URL (optional) - Status post.Status `json:"status,omitempty"` // Post status (default: draft) -} - -// UpdatePostRequest represents a request to update a post -type UpdatePostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - AuthorUID string `json:"author_uid"` // Author user UID (for authorization) - Title *string `json:"title,omitempty"` // Post title (optional) - Content *string `json:"content,omitempty"` // Post content (optional) - Type *post.Type `json:"type,omitempty"` // Post type (optional) - CategoryID *gocql.UUID `json:"category_id,omitempty"` // Category ID (optional) - Tags []string `json:"tags,omitempty"` // Post tags (optional) - Images []string `json:"images,omitempty"` // Image URLs (optional) - VideoURL *string `json:"video_url,omitempty"` // Video URL (optional) - LinkURL *string `json:"link_url,omitempty"` // Link URL (optional) -} - -// GetPostRequest represents a request to get a post -type GetPostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - UserUID *string `json:"user_uid,omitempty"` // User UID (for view count increment) -} - -// DeletePostRequest represents a request to delete a post -type DeletePostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - AuthorUID string `json:"author_uid"` // Author user UID (for authorization) -} - -// PublishPostRequest represents a request to publish a post -type PublishPostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - AuthorUID string `json:"author_uid"` // Author user UID (for authorization) -} - -// ArchivePostRequest represents a request to archive a post -type ArchivePostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - AuthorUID string `json:"author_uid"` // Author user UID (for authorization) -} - -// ListPostsRequest represents a request to list posts -type ListPostsRequest struct { - CategoryID *gocql.UUID `json:"category_id,omitempty"` // Category ID (optional) - Tag *string `json:"tag,omitempty"` // Tag name (optional) - Status *post.Status `json:"status,omitempty"` // Post status (optional) - Type *post.Type `json:"type,omitempty"` // Post type (optional) - AuthorUID *string `json:"author_uid,omitempty"` // Author UID (optional) - CreateStartTime *int64 `json:"create_start_time,omitempty"` // Create start time (optional) - CreateEndTime *int64 `json:"create_end_time,omitempty"` // Create end time (optional) - PageSize int64 `json:"page_size"` // Page size - PageIndex int64 `json:"page_index"` // Page index - OrderBy string `json:"order_by,omitempty"` // Order by field - OrderDirection string `json:"order_direction,omitempty"` // Order direction (ASC/DESC) -} - -// ListPostsByAuthorRequest represents a request to list posts by author -type ListPostsByAuthorRequest struct { - AuthorUID string `json:"author_uid"` // Author UID - Status *post.Status `json:"status,omitempty"` // Post status (optional) - PageSize int64 `json:"page_size"` // Page size - PageIndex int64 `json:"page_index"` // Page index -} - -// ListPostsByCategoryRequest represents a request to list posts by category -type ListPostsByCategoryRequest struct { - CategoryID gocql.UUID `json:"category_id"` // Category ID - Status *post.Status `json:"status,omitempty"` // Post status (optional) - PageSize int64 `json:"page_size"` // Page size - PageIndex int64 `json:"page_index"` // Page index -} - -// ListPostsByTagRequest represents a request to list posts by tag -type ListPostsByTagRequest struct { - Tag string `json:"tag"` // Tag name - Status *post.Status `json:"status,omitempty"` // Post status (optional) - PageSize int64 `json:"page_size"` // Page size - PageIndex int64 `json:"page_index"` // Page index -} - -// GetPinnedPostsRequest represents a request to get pinned posts -type GetPinnedPostsRequest struct { - Limit int64 `json:"limit,omitempty"` // Limit (optional, default: 10) -} - -// LikePostRequest represents a request to like a post -type LikePostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - UserUID string `json:"user_uid"` // User UID -} - -// UnlikePostRequest represents a request to unlike a post -type UnlikePostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - UserUID string `json:"user_uid"` // User UID -} - -// ViewPostRequest represents a request to view a post -type ViewPostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - UserUID *string `json:"user_uid,omitempty"` // User UID (optional) -} - -// PinPostRequest represents a request to pin a post -type PinPostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - AuthorUID string `json:"author_uid"` // Author user UID (for authorization) -} - -// UnpinPostRequest represents a request to unpin a post -type UnpinPostRequest struct { - PostID gocql.UUID `json:"post_id"` // Post ID - AuthorUID string `json:"author_uid"` // Author user UID (for authorization) -} - -// PostResponse represents a post response -type PostResponse struct { - ID gocql.UUID `json:"id"` - AuthorUID string `json:"author_uid"` - Title string `json:"title"` - Content string `json:"content"` - Type post.Type `json:"type"` - Status post.Status `json:"status"` - CategoryID *gocql.UUID `json:"category_id,omitempty"` - Tags []string `json:"tags,omitempty"` - Images []string `json:"images,omitempty"` - VideoURL *string `json:"video_url,omitempty"` - LinkURL *string `json:"link_url,omitempty"` - LikeCount int64 `json:"like_count"` - CommentCount int64 `json:"comment_count"` - ViewCount int64 `json:"view_count"` - IsPinned bool `json:"is_pinned"` - PinnedAt *int64 `json:"pinned_at,omitempty"` - PublishedAt *int64 `json:"published_at,omitempty"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -// ListPostsResponse represents a list of posts response -type ListPostsResponse struct { - Data []PostResponse `json:"data"` - Page Pager `json:"page"` -} - -// Pager represents pagination information -type Pager struct { - PageIndex int64 `json:"page_index"` - PageSize int64 `json:"page_size"` - Total int64 `json:"total"` - TotalPage int64 `json:"total_page"` -} - diff --git a/pkg/post/repository/category.go b/pkg/post/repository/category.go deleted file mode 100644 index f01b1a2..0000000 --- a/pkg/post/repository/category.go +++ /dev/null @@ -1,263 +0,0 @@ -package repository - -import ( - "context" - "fmt" - "strings" - - "backend/pkg/library/cassandra" - "backend/pkg/post/domain/entity" - domainRepo "backend/pkg/post/domain/repository" - - "github.com/gocql/gocql" -) - -// CategoryRepositoryParam 定義 CategoryRepository 的初始化參數 -type CategoryRepositoryParam struct { - DB *cassandra.DB - Keyspace string -} - -// CategoryRepository 實作 domain repository 介面 -type CategoryRepository struct { - repo cassandra.Repository[*entity.Category] - db *cassandra.DB - keyspace string -} - -// NewCategoryRepository 創建新的 CategoryRepository -func NewCategoryRepository(param CategoryRepositoryParam) domainRepo.CategoryRepository { - repo, err := cassandra.NewRepository[*entity.Category](param.DB, param.Keyspace) - if err != nil { - panic(fmt.Sprintf("failed to create category repository: %v", err)) - } - - keyspace := param.Keyspace - if keyspace == "" { - keyspace = param.DB.GetDefaultKeyspace() - } - - return &CategoryRepository{ - repo: repo, - db: param.DB, - keyspace: keyspace, - } -} - -// Insert 插入單筆分類 -func (r *CategoryRepository) Insert(ctx context.Context, data *entity.Category) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - if data.ParentID == nil { - data.ParentID = &gocql.UUID{} - } - - // 設置時間戳 - data.SetTimestamps() - - // 如果是新分類,生成 ID - if data.IsNew() { - data.ID = gocql.TimeUUID() - } - - // Slug 轉為小寫 - data.Slug = strings.ToLower(strings.TrimSpace(data.Slug)) - - return r.repo.Insert(ctx, data) -} - -// FindOne 根據 ID 查詢單筆分類 -func (r *CategoryRepository) FindOne(ctx context.Context, id string) (*entity.Category, error) { - var zeroUUID gocql.UUID - uuid, err := gocql.ParseUUID(id) - if err != nil { - return nil, err - } - - if uuid == zeroUUID { - return nil, ErrInvalidInput - } - - category, err := r.repo.Get(ctx, id) - if err != nil { - if cassandra.IsNotFound(err) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to find category: %w", err) - } - - return category, nil -} - -// Update 更新分類 -func (r *CategoryRepository) Update(ctx context.Context, data *entity.Category) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - // 更新時間戳 - data.SetTimestamps() - - // Slug 轉為小寫 - data.Slug = strings.ToLower(strings.TrimSpace(data.Slug)) - - return r.repo.Update(ctx, data) -} - -// Delete 刪除分類 -func (r *CategoryRepository) Delete(ctx context.Context, id string) error { - var zeroUUID gocql.UUID - uuid, err := gocql.ParseUUID(id) - if err != nil { - return err - } - if uuid == zeroUUID { - return ErrInvalidInput - } - - return r.repo.Delete(ctx, id) -} - -// FindBySlug 根據 slug 查詢分類 -func (r *CategoryRepository) FindBySlug(ctx context.Context, slug string) (*entity.Category, error) { - if slug == "" { - return nil, ErrInvalidInput - } - - // 標準化 slug - slug = strings.ToLower(strings.TrimSpace(slug)) - - // 構建查詢(要有 SAI 索引在 slug 欄位上) - query := r.repo.Query().Where(cassandra.Eq("slug", slug)) - - var categories []*entity.Category - if err := query.Scan(ctx, &categories); err != nil { - if cassandra.IsNotFound(err) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to query category: %w", err) - } - - if len(categories) == 0 { - return nil, ErrNotFound - } - - return categories[0], nil -} - -// FindByParentID 根據父分類 ID 查詢子分類 -func (r *CategoryRepository) FindByParentID(ctx context.Context, parentID string) ([]*entity.Category, error) { - query := r.repo.Query() - var zeroUUID gocql.UUID - if parentID != "" { - // 構建查詢(有 SAI 索引在 parentID 欄位上) - uuid, err := gocql.ParseUUID(parentID) - if err != nil { - return nil, err - } - if uuid != zeroUUID { - query = query.Where(cassandra.Eq("parent_id", uuid)) - } - } else { - query = query.Where(cassandra.Eq("parent_id", zeroUUID)) - } - - // 按 sort_order 排序 - query = query.OrderBy("sort_order", cassandra.ASC) - - var categories []*entity.Category - if err := query.Scan(ctx, &categories); err != nil { - return nil, fmt.Errorf("failed to query categories: %w", err) - } - - return categories, nil -} - -// FindRootCategories 查詢根分類 -func (r *CategoryRepository) FindRootCategories(ctx context.Context) ([]*entity.Category, error) { - return r.FindByParentID(ctx, "") -} - -// FindActive 查詢啟用的分類 -func (r *CategoryRepository) FindActive(ctx context.Context) ([]*entity.Category, error) { - query := r.repo.Query(). - Where(cassandra.Eq("is_active", true)). - OrderBy("sort_order", cassandra.ASC) - - var categories []*entity.Category - if err := query.Scan(ctx, &categories); err != nil { - return nil, fmt.Errorf("failed to query active categories: %w", err) - } - - result := categories - - return result, nil -} - -// IncrementPostCount 增加貼文數(使用 counter 原子操作避免競爭條件) -// 注意:post_count 欄位必須是 counter 類型 -func (r *CategoryRepository) IncrementPostCount(ctx context.Context, categoryID string) error { - uuid, err := gocql.ParseUUID(categoryID) - if err != nil { - return fmt.Errorf("%w: invalid category ID: %v", ErrInvalidInput, err) - } - - // 使用 counter 原子更新操作:UPDATE categories SET post_count = post_count + 1 WHERE id = ? - var zeroCategory entity.Category - tableName := zeroCategory.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET post_count = post_count + 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(uuid) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to increment post count: %w", err) - } - - return nil -} - -// DecrementPostCount 減少貼文數(使用 counter 原子操作避免競爭條件) -// 注意:post_count 欄位必須是 counter 類型 -func (r *CategoryRepository) DecrementPostCount(ctx context.Context, categoryID string) error { - uuid, err := gocql.ParseUUID(categoryID) - if err != nil { - return fmt.Errorf("%w: invalid category ID: %v", ErrInvalidInput, err) - } - - // 使用 counter 原子更新操作:UPDATE categories SET post_count = post_count - 1 WHERE id = ? - var zeroCategory entity.Category - tableName := zeroCategory.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET post_count = post_count - 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(uuid) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to decrement post count: %w", err) - } - - return nil -} diff --git a/pkg/post/repository/comment.go b/pkg/post/repository/comment.go deleted file mode 100644 index 6a4f9e1..0000000 --- a/pkg/post/repository/comment.go +++ /dev/null @@ -1,383 +0,0 @@ -package repository - -import ( - "context" - "fmt" - - "backend/pkg/library/cassandra" - "backend/pkg/post/domain/entity" - "backend/pkg/post/domain/post" - domainRepo "backend/pkg/post/domain/repository" - - "github.com/gocql/gocql" -) - -// CommentRepositoryParam 定義 CommentRepository 的初始化參數 -type CommentRepositoryParam struct { - DB *cassandra.DB - Keyspace string -} - -// CommentRepository 實作 domain repository 介面 -type CommentRepository struct { - repo cassandra.Repository[*entity.Comment] - db *cassandra.DB - keyspace string -} - -// NewCommentRepository 創建新的 CommentRepository -func NewCommentRepository(param CommentRepositoryParam) domainRepo.CommentRepository { - repo, err := cassandra.NewRepository[*entity.Comment](param.DB, param.Keyspace) - if err != nil { - panic(fmt.Sprintf("failed to create comment repository: %v", err)) - } - - keyspace := param.Keyspace - if keyspace == "" { - keyspace = param.DB.GetDefaultKeyspace() - } - - return &CommentRepository{ - repo: repo, - db: param.DB, - keyspace: keyspace, - } -} - -// Insert 插入單筆評論 -func (r *CommentRepository) Insert(ctx context.Context, data *entity.Comment) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - // 設置時間戳 - data.SetTimestamps() - - // 如果是新評論,生成 ID - if data.IsNew() { - data.ID = gocql.TimeUUID() - } - - return r.repo.Insert(ctx, data) -} - -// FindOne 根據 ID 查詢單筆評論 -func (r *CommentRepository) FindOne(ctx context.Context, id gocql.UUID) (*entity.Comment, error) { - var zeroUUID gocql.UUID - if id == zeroUUID { - return nil, ErrInvalidInput - } - - comment, err := r.repo.Get(ctx, id) - if err != nil { - if cassandra.IsNotFound(err) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to find comment: %w", err) - } - - return comment, nil -} - -// Update 更新評論 -func (r *CommentRepository) Update(ctx context.Context, data *entity.Comment) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - // 更新時間戳 - data.SetTimestamps() - - return r.repo.Update(ctx, data) -} - -// Delete 刪除評論(軟刪除) -func (r *CommentRepository) Delete(ctx context.Context, id gocql.UUID) error { - var zeroUUID gocql.UUID - if id == zeroUUID { - return ErrInvalidInput - } - - // 先查詢評論 - comment, err := r.FindOne(ctx, id) - if err != nil { - return err - } - - // 軟刪除:標記為已刪除 - comment.Delete() - return r.Update(ctx, comment) -} - -// FindByPostID 根據貼文 ID 查詢評論 -func (r *CommentRepository) FindByPostID(ctx context.Context, postID gocql.UUID, params *domainRepo.CommentQueryParams) ([]*entity.Comment, int64, error) { - var zeroUUID gocql.UUID - if postID == zeroUUID { - return nil, 0, ErrInvalidInput - } - - // 構建查詢(使用 PostID 作為 clustering key) - query := r.repo.Query().Where(cassandra.Eq("post_id", postID)) - - // 添加父評論過濾(如果指定,只查詢回覆) - if params != nil && params.ParentID != nil { - query = query.Where(cassandra.Eq("parent_id", *params.ParentID)) - } else { - // 如果沒有指定 ParentID,只查詢頂層評論(parent_id 為 null) - // 注意:Cassandra 不支援直接查詢 null,需要特殊處理 - // 這裡簡化處理,實際可能需要使用 Materialized View - } - - // 添加狀態過濾 - if params != nil && params.Status != nil { - query = query.Where(cassandra.Eq("status", *params.Status)) - } else { - // 預設只查詢已發布的評論 - published := post.CommentStatusPublished - query = query.Where(cassandra.Eq("status", published)) - } - - // 添加排序 - orderBy := "created_at" - if params != nil && params.OrderBy != "" { - orderBy = params.OrderBy - } - order := cassandra.ASC - if params != nil && params.OrderDirection == "DESC" { - order = cassandra.DESC - } - query = query.OrderBy(orderBy, order) - - // 添加分頁 - pageSize := int64(20) - if params != nil && params.PageSize > 0 { - pageSize = params.PageSize - } - limit := int(pageSize) - query = query.Limit(limit) - - // 執行查詢 - var comments []*entity.Comment - if err := query.Scan(ctx, &comments); err != nil { - return nil, 0, fmt.Errorf("failed to query comments: %w", err) - } - - result := comments - - total := int64(len(result)) - return result, total, nil -} - -// FindByParentID 根據父評論 ID 查詢回覆 -func (r *CommentRepository) FindByParentID(ctx context.Context, parentID gocql.UUID, params *domainRepo.CommentQueryParams) ([]*entity.Comment, int64, error) { - var zeroUUID gocql.UUID - if parentID == zeroUUID { - return nil, 0, ErrInvalidInput - } - - query := r.repo.Query().Where(cassandra.Eq("parent_id", parentID)) - - // 添加狀態過濾 - if params != nil && params.Status != nil { - query = query.Where(cassandra.Eq("status", *params.Status)) - } else { - published := post.CommentStatusPublished - query = query.Where(cassandra.Eq("status", published)) - } - - // 添加排序和分頁 - orderBy := "created_at" - if params != nil && params.OrderBy != "" { - orderBy = params.OrderBy - } - order := cassandra.ASC - if params != nil && params.OrderDirection == "DESC" { - order = cassandra.DESC - } - query = query.OrderBy(orderBy, order) - - pageSize := int64(20) - if params != nil && params.PageSize > 0 { - pageSize = params.PageSize - } - query = query.Limit(int(pageSize)) - - var comments []*entity.Comment - if err := query.Scan(ctx, &comments); err != nil { - return nil, 0, fmt.Errorf("failed to query replies: %w", err) - } - - return comments, int64(len(comments)), nil -} - -// FindByAuthorUID 根據作者 UID 查詢評論 -func (r *CommentRepository) FindByAuthorUID(ctx context.Context, authorUID string, params *domainRepo.CommentQueryParams) ([]*entity.Comment, int64, error) { - if authorUID == "" { - return nil, 0, ErrInvalidInput - } - - query := r.repo.Query().Where(cassandra.Eq("author_uid", authorUID)) - - // 添加狀態過濾 - if params != nil && params.Status != nil { - query = query.Where(cassandra.Eq("status", *params.Status)) - } - - // 添加排序和分頁 - orderBy := "created_at" - if params != nil && params.OrderBy != "" { - orderBy = params.OrderBy - } - order := cassandra.DESC - if params != nil && params.OrderDirection == "ASC" { - order = cassandra.ASC - } - query = query.OrderBy(orderBy, order) - - pageSize := int64(20) - if params != nil && params.PageSize > 0 { - pageSize = params.PageSize - } - query = query.Limit(int(pageSize)) - - var comments []*entity.Comment - if err := query.Scan(ctx, &comments); err != nil { - return nil, 0, fmt.Errorf("failed to query comments: %w", err) - } - - return comments, int64(len(comments)), nil -} - -// FindReplies 查詢指定評論的回覆 -func (r *CommentRepository) FindReplies(ctx context.Context, commentID gocql.UUID, params *domainRepo.CommentQueryParams) ([]*entity.Comment, int64, error) { - return r.FindByParentID(ctx, commentID, params) -} - -// IncrementLikeCount 增加按讚數(使用 counter 原子操作避免競爭條件) -// 注意:like_count 欄位必須是 counter 類型 -func (r *CommentRepository) IncrementLikeCount(ctx context.Context, commentID gocql.UUID) error { - var zeroUUID gocql.UUID - if commentID == zeroUUID { - return ErrInvalidInput - } - - var zeroComment entity.Comment - tableName := zeroComment.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET like_count = like_count + 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(commentID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to increment like count: %w", err) - } - - return nil -} - -// DecrementLikeCount 減少按讚數(使用 counter 原子操作避免競爭條件) -// 注意:like_count 欄位必須是 counter 類型 -func (r *CommentRepository) DecrementLikeCount(ctx context.Context, commentID gocql.UUID) error { - var zeroUUID gocql.UUID - if commentID == zeroUUID { - return ErrInvalidInput - } - - var zeroComment entity.Comment - tableName := zeroComment.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET like_count = like_count - 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(commentID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to decrement like count: %w", err) - } - - return nil -} - -// IncrementReplyCount 增加回覆數(使用 counter 原子操作避免競爭條件) -// 注意:reply_count 欄位必須是 counter 類型 -func (r *CommentRepository) IncrementReplyCount(ctx context.Context, commentID gocql.UUID) error { - var zeroUUID gocql.UUID - if commentID == zeroUUID { - return ErrInvalidInput - } - - var zeroComment entity.Comment - tableName := zeroComment.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET reply_count = reply_count + 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(commentID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to increment reply count: %w", err) - } - - return nil -} - -// DecrementReplyCount 減少回覆數(使用 counter 原子操作避免競爭條件) -// 注意:reply_count 欄位必須是 counter 類型 -func (r *CommentRepository) DecrementReplyCount(ctx context.Context, commentID gocql.UUID) error { - var zeroUUID gocql.UUID - if commentID == zeroUUID { - return ErrInvalidInput - } - - var zeroComment entity.Comment - tableName := zeroComment.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET reply_count = reply_count - 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(commentID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to decrement reply count: %w", err) - } - - return nil -} - -// UpdateStatus 更新評論狀態 -func (r *CommentRepository) UpdateStatus(ctx context.Context, commentID gocql.UUID, status post.CommentStatus) error { - comment, err := r.FindOne(ctx, commentID) - if err != nil { - return err - } - - comment.Status = status - return r.Update(ctx, comment) -} diff --git a/pkg/post/repository/error.go b/pkg/post/repository/error.go deleted file mode 100644 index 0aa7efe..0000000 --- a/pkg/post/repository/error.go +++ /dev/null @@ -1,34 +0,0 @@ -package repository - -import ( - "errors" - - "backend/pkg/library/cassandra" -) - -// Common repository errors -var ( - // ErrNotFound is returned when a requested resource is not found - ErrNotFound = errors.New("resource not found") - - // ErrInvalidInput is returned when input validation fails - ErrInvalidInput = errors.New("invalid input") - - // ErrDuplicateKey is returned when attempting to insert a document with a duplicate key - ErrDuplicateKey = errors.New("duplicate key error") -) - -// IsNotFound checks if the error is a not found error -func IsNotFound(err error) bool { - if err == nil { - return false - } - if err == ErrNotFound { - return true - } - if cassandra.IsNotFound(err) { - return true - } - return false -} - diff --git a/pkg/post/repository/like.go b/pkg/post/repository/like.go deleted file mode 100644 index bd8d999..0000000 --- a/pkg/post/repository/like.go +++ /dev/null @@ -1,228 +0,0 @@ -package repository - -import ( - "context" - "fmt" - - "backend/pkg/library/cassandra" - "backend/pkg/post/domain/entity" - domainRepo "backend/pkg/post/domain/repository" - - "github.com/gocql/gocql" -) - -// LikeRepositoryParam 定義 LikeRepository 的初始化參數 -type LikeRepositoryParam struct { - DB *cassandra.DB - Keyspace string -} - -// LikeRepository 實作 domain repository 介面 -type LikeRepository struct { - repo cassandra.Repository[*entity.Like] - db *cassandra.DB -} - -// NewLikeRepository 創建新的 LikeRepository -func NewLikeRepository(param LikeRepositoryParam) domainRepo.LikeRepository { - repo, err := cassandra.NewRepository[*entity.Like](param.DB, param.Keyspace) - if err != nil { - panic(fmt.Sprintf("failed to create like repository: %v", err)) - } - - return &LikeRepository{ - repo: repo, - db: param.DB, - } -} - -// Insert 插入單筆按讚 -func (r *LikeRepository) Insert(ctx context.Context, data *entity.Like) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - // 設置時間戳 - data.SetTimestamps() - - // 如果是新按讚,生成 ID - if data.IsNew() { - data.ID = gocql.TimeUUID() - } - - return r.repo.Insert(ctx, data) -} - -// FindOne 根據 ID 查詢單筆按讚 -func (r *LikeRepository) FindOne(ctx context.Context, id gocql.UUID) (*entity.Like, error) { - var zeroUUID gocql.UUID - if id == zeroUUID { - return nil, ErrInvalidInput - } - - like, err := r.repo.Get(ctx, id) - if err != nil { - if cassandra.IsNotFound(err) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to find like: %w", err) - } - - return like, nil -} - -// Delete 刪除按讚 -func (r *LikeRepository) Delete(ctx context.Context, id gocql.UUID) error { - var zeroUUID gocql.UUID - if id == zeroUUID { - return ErrInvalidInput - } - - return r.repo.Delete(ctx, id) -} - -// FindByTargetID 根據目標 ID 查詢按讚列表 -func (r *LikeRepository) FindByTargetID(ctx context.Context, targetID gocql.UUID, targetType string) ([]*entity.Like, error) { - var zeroUUID gocql.UUID - if targetID == zeroUUID { - return nil, ErrInvalidInput - } - - if targetType != "post" && targetType != "comment" { - return nil, ErrInvalidInput - } - - // 構建查詢 - query := r.repo.Query(). - Where(cassandra.Eq("target_id", targetID)). - Where(cassandra.Eq("target_type", targetType)). - OrderBy("created_at", cassandra.DESC) - - var likes []*entity.Like - if err := query.Scan(ctx, &likes); err != nil { - return nil, fmt.Errorf("failed to query likes: %w", err) - } - - return likes, nil -} - -// FindByUserUID 根據用戶 UID 查詢按讚列表 -func (r *LikeRepository) FindByUserUID(ctx context.Context, userUID string, params *domainRepo.LikeQueryParams) ([]*entity.Like, int64, error) { - if userUID == "" { - return nil, 0, ErrInvalidInput - } - - query := r.repo.Query().Where(cassandra.Eq("user_uid", userUID)) - - // 添加目標類型過濾 - if params != nil && params.TargetType != nil { - query = query.Where(cassandra.Eq("target_type", *params.TargetType)) - } - - // 添加目標 ID 過濾 - if params != nil && params.TargetID != nil { - query = query.Where(cassandra.Eq("target_id", *params.TargetID)) - } - - // 添加排序 - orderBy := "created_at" - if params != nil && params.OrderBy != "" { - orderBy = params.OrderBy - } - order := cassandra.DESC - if params != nil && params.OrderDirection == "ASC" { - order = cassandra.ASC - } - query = query.OrderBy(orderBy, order) - - // 添加分頁 - pageSize := int64(20) - if params != nil && params.PageSize > 0 { - pageSize = params.PageSize - } - query = query.Limit(int(pageSize)) - - var likes []*entity.Like - if err := query.Scan(ctx, &likes); err != nil { - return nil, 0, fmt.Errorf("failed to query likes: %w", err) - } - - result := likes - - return result, int64(len(result)), nil -} - -// FindByTargetAndUser 根據目標和用戶查詢按讚 -func (r *LikeRepository) FindByTargetAndUser(ctx context.Context, targetID gocql.UUID, userUID string, targetType string) (*entity.Like, error) { - var zeroUUID gocql.UUID - if targetID == zeroUUID || userUID == "" { - return nil, ErrInvalidInput - } - - if targetType != "post" && targetType != "comment" { - return nil, ErrInvalidInput - } - - // 構建查詢 - query := r.repo.Query(). - Where(cassandra.Eq("target_id", targetID)). - Where(cassandra.Eq("user_uid", userUID)). - Where(cassandra.Eq("target_type", targetType)). - Limit(1) - - var likes []*entity.Like - if err := query.Scan(ctx, &likes); err != nil { - if cassandra.IsNotFound(err) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to query like: %w", err) - } - - if len(likes) == 0 { - return nil, ErrNotFound - } - - return likes[0], nil -} - -// CountByTargetID 計算目標的按讚數 -func (r *LikeRepository) CountByTargetID(ctx context.Context, targetID gocql.UUID, targetType string) (int64, error) { - var zeroUUID gocql.UUID - if targetID == zeroUUID { - return 0, ErrInvalidInput - } - - if targetType != "post" && targetType != "comment" { - return 0, ErrInvalidInput - } - - // 構建查詢 - query := r.repo.Query(). - Where(cassandra.Eq("target_id", targetID)). - Where(cassandra.Eq("target_type", targetType)) - - count, err := query.Count(ctx) - if err != nil { - return 0, fmt.Errorf("failed to count likes: %w", err) - } - - return count, nil -} - -// DeleteByTargetAndUser 根據目標和用戶刪除按讚 -func (r *LikeRepository) DeleteByTargetAndUser(ctx context.Context, targetID gocql.UUID, userUID string, targetType string) error { - // 先查詢按讚 - like, err := r.FindByTargetAndUser(ctx, targetID, userUID, targetType) - if err != nil { - return err - } - - // 刪除按讚 - return r.Delete(ctx, like.ID) -} - diff --git a/pkg/post/repository/post.go b/pkg/post/repository/post.go deleted file mode 100644 index 148de1f..0000000 --- a/pkg/post/repository/post.go +++ /dev/null @@ -1,511 +0,0 @@ -package repository - -import ( - "context" - "fmt" - "math" - - "backend/pkg/library/cassandra" - "backend/pkg/post/domain/entity" - "backend/pkg/post/domain/post" - domainRepo "backend/pkg/post/domain/repository" - - "github.com/gocql/gocql" -) - -// PostRepositoryParam 定義 PostRepository 的初始化參數 -type PostRepositoryParam struct { - DB *cassandra.DB - Keyspace string -} - -// PostRepository 實作 domain repository 介面 -type PostRepository struct { - repo cassandra.Repository[*entity.Post] - db *cassandra.DB - keyspace string -} - -// NewPostRepository 創建新的 PostRepository -func NewPostRepository(param PostRepositoryParam) domainRepo.PostRepository { - repo, err := cassandra.NewRepository[*entity.Post](param.DB, param.Keyspace) - if err != nil { - panic(fmt.Sprintf("failed to create post repository: %v", err)) - } - - keyspace := param.Keyspace - if keyspace == "" { - keyspace = param.DB.GetDefaultKeyspace() - } - - return &PostRepository{ - repo: repo, - db: param.DB, - keyspace: keyspace, - } -} - -// Insert 插入單筆貼文 -func (r *PostRepository) Insert(ctx context.Context, data *entity.Post) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - // 設置時間戳 - data.SetTimestamps() - - // 如果是新貼文,生成 ID - if data.IsNew() { - data.ID = gocql.TimeUUID() - } - - // 如果狀態是 published,設置發布時間 - if data.Status == post.PostStatusPublished && data.PublishedAt == nil { - now := data.CreatedAt - data.PublishedAt = &now - } - - return r.repo.Insert(ctx, data) -} - -// FindOne 根據 ID 查詢單筆貼文 -func (r *PostRepository) FindOne(ctx context.Context, id gocql.UUID) (*entity.Post, error) { - var zeroUUID gocql.UUID - if id == zeroUUID { - return nil, ErrInvalidInput - } - - post, err := r.repo.Get(ctx, id) - if err != nil { - if cassandra.IsNotFound(err) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to find post: %w", err) - } - - return post, nil -} - -// Update 更新貼文 -func (r *PostRepository) Update(ctx context.Context, data *entity.Post) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - // 更新時間戳 - data.SetTimestamps() - - return r.repo.Update(ctx, data) -} - -// Delete 刪除貼文(軟刪除) -func (r *PostRepository) Delete(ctx context.Context, id gocql.UUID) error { - var zeroUUID gocql.UUID - if id == zeroUUID { - return ErrInvalidInput - } - - // 先查詢貼文 - post, err := r.FindOne(ctx, id) - if err != nil { - return err - } - - // 軟刪除:標記為已刪除 - post.Delete() - return r.Update(ctx, post) -} - -// FindByAuthorUID 根據作者 UID 查詢貼文 -func (r *PostRepository) FindByAuthorUID(ctx context.Context, authorUID string, params *domainRepo.PostQueryParams) ([]*entity.Post, int64, error) { - if authorUID == "" { - return nil, 0, ErrInvalidInput - } - - // 構建查詢 - query := r.repo.Query().Where(cassandra.Eq("author_uid", authorUID)) - - // 添加狀態過濾 - if params != nil && params.Status != nil { - query = query.Where(cassandra.Eq("status", *params.Status)) - } - - // 添加排序 - orderBy := "created_at" - if params != nil && params.OrderBy != "" { - orderBy = params.OrderBy - } - order := cassandra.DESC - if params != nil && params.OrderDirection == "ASC" { - order = cassandra.ASC - } - query = query.OrderBy(orderBy, order) - - // 添加分頁 - pageSize := int64(20) - if params != nil && params.PageSize > 0 { - pageSize = params.PageSize - } - pageIndex := int64(1) - if params != nil && params.PageIndex > 0 { - pageIndex = params.PageIndex - } - limit := int(pageSize) - query = query.Limit(limit) - - // 執行查詢 - var posts []*entity.Post - if err := query.Scan(ctx, &posts); err != nil { - return nil, 0, fmt.Errorf("failed to query posts: %w", err) - } - - result := posts - - // 計算總數(簡化實作,實際應該使用 COUNT 查詢) - total := int64(len(posts)) - if params != nil && params.PageIndex > 1 { - // 這裡應該執行 COUNT 查詢,但為了簡化,我們假設有更多結果 - total = pageSize * pageIndex - } - - return result, total, nil -} - -// FindByCategoryID 根據分類 ID 查詢貼文 -func (r *PostRepository) FindByCategoryID(ctx context.Context, categoryID gocql.UUID, params *domainRepo.PostQueryParams) ([]*entity.Post, int64, error) { - var zeroUUID gocql.UUID - if categoryID == zeroUUID { - return nil, 0, ErrInvalidInput - } - - // 構建查詢 - query := r.repo.Query().Where(cassandra.Eq("category_id", categoryID)) - - // 添加狀態過濾 - if params != nil && params.Status != nil { - query = query.Where(cassandra.Eq("status", *params.Status)) - } - - // 添加排序和分頁(類似 FindByAuthorUID) - orderBy := "created_at" - if params != nil && params.OrderBy != "" { - orderBy = params.OrderBy - } - order := cassandra.DESC - if params != nil && params.OrderDirection == "ASC" { - order = cassandra.ASC - } - query = query.OrderBy(orderBy, order) - - pageSize := int64(20) - if params != nil && params.PageSize > 0 { - pageSize = params.PageSize - } - limit := int(pageSize) - query = query.Limit(limit) - - var posts []*entity.Post - if err := query.Scan(ctx, &posts); err != nil { - return nil, 0, fmt.Errorf("failed to query posts: %w", err) - } - - result := posts - - total := int64(len(posts)) - return result, total, nil -} - -// FindByTag 根據標籤查詢貼文 -func (r *PostRepository) FindByTag(ctx context.Context, tagName string, params *domainRepo.PostQueryParams) ([]*entity.Post, int64, error) { - if tagName == "" { - return nil, 0, ErrInvalidInput - } - - // 構建查詢(注意:Cassandra 的集合查詢需要使用 CONTAINS,這裡簡化處理) - // 實際實作中,可能需要使用 SAI 索引或 Materialized View - query := r.repo.Query() - - // 添加狀態過濾 - if params != nil && params.Status != nil { - query = query.Where(cassandra.Eq("status", *params.Status)) - } - - // 添加排序和分頁 - orderBy := "created_at" - if params != nil && params.OrderBy != "" { - orderBy = params.OrderBy - } - order := cassandra.DESC - if params != nil && params.OrderDirection == "ASC" { - order = cassandra.ASC - } - query = query.OrderBy(orderBy, order) - - pageSize := int64(20) - if params != nil && params.PageSize > 0 { - pageSize = params.PageSize - } - limit := int(pageSize) - query = query.Limit(limit) - - var posts []*entity.Post - if err := query.Scan(ctx, &posts); err != nil { - return nil, 0, fmt.Errorf("failed to query posts: %w", err) - } - - // 過濾包含指定標籤的貼文 - filtered := make([]*entity.Post, 0) - for _, p := range posts { - for _, tag := range p.Tags { - if tag == tagName { - filtered = append(filtered, p) - break - } - } - } - - total := int64(len(filtered)) - return filtered, total, nil -} - -// FindPinnedPosts 查詢置頂貼文 -func (r *PostRepository) FindPinnedPosts(ctx context.Context, limit int64) ([]*entity.Post, error) { - query := r.repo.Query(). - Where(cassandra.Eq("is_pinned", true)). - Where(cassandra.Eq("status", post.PostStatusPublished)). - OrderBy("pinned_at", cassandra.DESC). - Limit(int(limit)) - - var posts []*entity.Post - if err := query.Scan(ctx, &posts); err != nil { - return nil, fmt.Errorf("failed to query pinned posts: %w", err) - } - - return posts, nil -} - -// FindByStatus 根據狀態查詢貼文 -func (r *PostRepository) FindByStatus(ctx context.Context, status post.Status, params *domainRepo.PostQueryParams) ([]*entity.Post, int64, error) { - query := r.repo.Query().Where(cassandra.Eq("status", status)) - - // 添加排序和分頁 - orderBy := "created_at" - if params != nil && params.OrderBy != "" { - orderBy = params.OrderBy - } - order := cassandra.DESC - if params != nil && params.OrderDirection == "ASC" { - order = cassandra.ASC - } - query = query.OrderBy(orderBy, order) - - pageSize := int64(20) - if params != nil && params.PageSize > 0 { - pageSize = params.PageSize - } - limit := int(pageSize) - query = query.Limit(limit) - - var posts []*entity.Post - if err := query.Scan(ctx, &posts); err != nil { - return nil, 0, fmt.Errorf("failed to query posts: %w", err) - } - - result := posts - - total := int64(len(posts)) - return result, total, nil -} - -// IncrementLikeCount 增加按讚數(使用 counter 原子操作避免競爭條件) -// 注意:like_count 欄位必須是 counter 類型 -func (r *PostRepository) IncrementLikeCount(ctx context.Context, postID gocql.UUID) error { - var zeroUUID gocql.UUID - if postID == zeroUUID { - return ErrInvalidInput - } - - var zeroPost entity.Post - tableName := zeroPost.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET like_count = like_count + 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(postID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to increment like count: %w", err) - } - - return nil -} - -// DecrementLikeCount 減少按讚數(使用 counter 原子操作避免競爭條件) -// 注意:like_count 欄位必須是 counter 類型 -func (r *PostRepository) DecrementLikeCount(ctx context.Context, postID gocql.UUID) error { - var zeroUUID gocql.UUID - if postID == zeroUUID { - return ErrInvalidInput - } - - var zeroPost entity.Post - tableName := zeroPost.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET like_count = like_count - 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(postID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to decrement like count: %w", err) - } - - return nil -} - -// IncrementCommentCount 增加評論數(使用 counter 原子操作避免競爭條件) -// 注意:comment_count 欄位必須是 counter 類型 -func (r *PostRepository) IncrementCommentCount(ctx context.Context, postID gocql.UUID) error { - var zeroUUID gocql.UUID - if postID == zeroUUID { - return ErrInvalidInput - } - - var zeroPost entity.Post - tableName := zeroPost.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET comment_count = comment_count + 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(postID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to increment comment count: %w", err) - } - - return nil -} - -// DecrementCommentCount 減少評論數(使用 counter 原子操作避免競爭條件) -// 注意:comment_count 欄位必須是 counter 類型 -func (r *PostRepository) DecrementCommentCount(ctx context.Context, postID gocql.UUID) error { - var zeroUUID gocql.UUID - if postID == zeroUUID { - return ErrInvalidInput - } - - var zeroPost entity.Post - tableName := zeroPost.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET comment_count = comment_count - 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(postID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to decrement comment count: %w", err) - } - - return nil -} - -// IncrementViewCount 增加瀏覽數(使用 counter 原子操作避免競爭條件) -// 注意:view_count 欄位必須是 counter 類型 -func (r *PostRepository) IncrementViewCount(ctx context.Context, postID gocql.UUID) error { - var zeroUUID gocql.UUID - if postID == zeroUUID { - return ErrInvalidInput - } - - var zeroPost entity.Post - tableName := zeroPost.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET view_count = view_count + 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(postID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to increment view count: %w", err) - } - - return nil -} - -// UpdateStatus 更新貼文狀態 -func (r *PostRepository) UpdateStatus(ctx context.Context, postID gocql.UUID, status post.Status) error { - postEntity, err := r.FindOne(ctx, postID) - if err != nil { - return err - } - - postEntity.Status = status - publishedStatus := post.PostStatusPublished - if status == publishedStatus && postEntity.PublishedAt == nil { - now := postEntity.UpdatedAt - postEntity.PublishedAt = &now - } - - return r.Update(ctx, postEntity) -} - -// PinPost 置頂貼文 -func (r *PostRepository) PinPost(ctx context.Context, postID gocql.UUID) error { - post, err := r.FindOne(ctx, postID) - if err != nil { - return err - } - - post.Pin() - return r.Update(ctx, post) -} - -// UnpinPost 取消置頂 -func (r *PostRepository) UnpinPost(ctx context.Context, postID gocql.UUID) error { - post, err := r.FindOne(ctx, postID) - if err != nil { - return err - } - - post.Unpin() - return r.Update(ctx, post) -} - -// calculateTotalPages 計算總頁數 -func calculateTotalPages(total, pageSize int64) int64 { - if pageSize <= 0 { - return 0 - } - return int64(math.Ceil(float64(total) / float64(pageSize))) -} - diff --git a/pkg/post/repository/tag.go b/pkg/post/repository/tag.go deleted file mode 100644 index 364e76c..0000000 --- a/pkg/post/repository/tag.go +++ /dev/null @@ -1,250 +0,0 @@ -package repository - -import ( - "context" - "fmt" - "strings" - - "backend/pkg/library/cassandra" - "backend/pkg/post/domain/entity" - domainRepo "backend/pkg/post/domain/repository" - - "github.com/gocql/gocql" -) - -// TagRepositoryParam 定義 TagRepository 的初始化參數 -type TagRepositoryParam struct { - DB *cassandra.DB - Keyspace string -} - -// TagRepository 實作 domain repository 介面 -type TagRepository struct { - repo cassandra.Repository[*entity.Tag] - db *cassandra.DB - keyspace string -} - -// NewTagRepository 創建新的 TagRepository -func NewTagRepository(param TagRepositoryParam) domainRepo.TagRepository { - repo, err := cassandra.NewRepository[*entity.Tag](param.DB, param.Keyspace) - if err != nil { - panic(fmt.Sprintf("failed to create tag repository: %v", err)) - } - - keyspace := param.Keyspace - if keyspace == "" { - keyspace = param.DB.GetDefaultKeyspace() - } - - return &TagRepository{ - repo: repo, - db: param.DB, - keyspace: keyspace, - } -} - -// Insert 插入單筆標籤 -func (r *TagRepository) Insert(ctx context.Context, data *entity.Tag) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - // 設置時間戳 - data.SetTimestamps() - - // 如果是新標籤,生成 ID - if data.IsNew() { - data.ID = gocql.TimeUUID() - } - - // 標籤名稱轉為小寫(統一格式) - data.Name = strings.ToLower(strings.TrimSpace(data.Name)) - - return r.repo.Insert(ctx, data) -} - -// FindOne 根據 ID 查詢單筆標籤 -func (r *TagRepository) FindOne(ctx context.Context, id gocql.UUID) (*entity.Tag, error) { - var zeroUUID gocql.UUID - if id == zeroUUID { - return nil, ErrInvalidInput - } - - tag, err := r.repo.Get(ctx, id) - if err != nil { - if cassandra.IsNotFound(err) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to find tag: %w", err) - } - - return tag, nil -} - -// Update 更新標籤 -func (r *TagRepository) Update(ctx context.Context, data *entity.Tag) error { - if data == nil { - return ErrInvalidInput - } - - // 驗證資料 - if err := data.Validate(); err != nil { - return fmt.Errorf("%w: %v", ErrInvalidInput, err) - } - - // 更新時間戳 - data.SetTimestamps() - - // 標籤名稱轉為小寫 - data.Name = strings.ToLower(strings.TrimSpace(data.Name)) - - return r.repo.Update(ctx, data) -} - -// Delete 刪除標籤 -func (r *TagRepository) Delete(ctx context.Context, id gocql.UUID) error { - var zeroUUID gocql.UUID - if id == zeroUUID { - return ErrInvalidInput - } - - return r.repo.Delete(ctx, id) -} - -// FindByName 根據名稱查詢標籤 -func (r *TagRepository) FindByName(ctx context.Context, name string) (*entity.Tag, error) { - if name == "" { - return nil, ErrInvalidInput - } - - // 標準化名稱 - name = strings.ToLower(strings.TrimSpace(name)) - - // 構建查詢(假設有 SAI 索引在 name 欄位上) - query := r.repo.Query().Where(cassandra.Eq("name", name)) - - var tags []*entity.Tag - if err := query.Scan(ctx, &tags); err != nil { - if cassandra.IsNotFound(err) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to query tag: %w", err) - } - - if len(tags) == 0 { - return nil, ErrNotFound - } - - return tags[0], nil -} - -// FindByNames 根據名稱列表查詢標籤 -func (r *TagRepository) FindByNames(ctx context.Context, names []string) ([]*entity.Tag, error) { - if len(names) == 0 { - return []*entity.Tag{}, nil - } - - // 標準化名稱 - normalizedNames := make([]string, len(names)) - for i, name := range names { - normalizedNames[i] = strings.ToLower(strings.TrimSpace(name)) - } - - // 構建查詢(使用 IN 條件) - query := r.repo.Query().Where(cassandra.In("name", toAnySlice(normalizedNames))) - - var tags []*entity.Tag - if err := query.Scan(ctx, &tags); err != nil { - return nil, fmt.Errorf("failed to query tags: %w", err) - } - - return tags, nil -} - -// FindPopular 查詢熱門標籤 -func (r *TagRepository) FindPopular(ctx context.Context, limit int64) ([]*entity.Tag, error) { - // 構建查詢,按 post_count 降序排列 - query := r.repo.Query(). - OrderBy("post_count", cassandra.DESC). - Limit(int(limit)) - - var tags []*entity.Tag - if err := query.Scan(ctx, &tags); err != nil { - return nil, fmt.Errorf("failed to query popular tags: %w", err) - } - - result := tags - - return result, nil -} - -// IncrementPostCount 增加貼文數(使用 counter 原子操作避免競爭條件) -// 注意:post_count 欄位必須是 counter 類型 -func (r *TagRepository) IncrementPostCount(ctx context.Context, tagID gocql.UUID) error { - var zeroUUID gocql.UUID - if tagID == zeroUUID { - return ErrInvalidInput - } - - // 使用 counter 原子更新操作:UPDATE tags SET post_count = post_count + 1 WHERE id = ? - var zeroTag entity.Tag - tableName := zeroTag.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET post_count = post_count + 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(tagID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to increment post count: %w", err) - } - - return nil -} - -// DecrementPostCount 減少貼文數(使用 counter 原子操作避免競爭條件) -// 注意:post_count 欄位必須是 counter 類型 -func (r *TagRepository) DecrementPostCount(ctx context.Context, tagID gocql.UUID) error { - var zeroUUID gocql.UUID - if tagID == zeroUUID { - return ErrInvalidInput - } - - // 使用 counter 原子更新操作:UPDATE tags SET post_count = post_count - 1 WHERE id = ? - var zeroTag entity.Tag - tableName := zeroTag.TableName() - if r.keyspace == "" { - return fmt.Errorf("%w: keyspace is required", ErrInvalidInput) - } - - stmt := fmt.Sprintf("UPDATE %s.%s SET post_count = post_count - 1 WHERE id = ?", r.keyspace, tableName) - query := r.db.GetSession().Query(stmt, nil). - WithContext(ctx). - Consistency(gocql.Quorum). - Bind(tagID) - - if err := query.ExecRelease(); err != nil { - return fmt.Errorf("failed to decrement post count: %w", err) - } - - return nil -} - -// toAnySlice 將 string slice 轉換為 []any -func toAnySlice(strs []string) []any { - result := make([]any, len(strs)) - for i, s := range strs { - result[i] = s - } - return result -} diff --git a/pkg/post/usecase/comment.go b/pkg/post/usecase/comment.go deleted file mode 100644 index 1e374ba..0000000 --- a/pkg/post/usecase/comment.go +++ /dev/null @@ -1,455 +0,0 @@ -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) -} - diff --git a/pkg/post/usecase/post.go b/pkg/post/usecase/post.go deleted file mode 100644 index df07a28..0000000 --- a/pkg/post/usecase/post.go +++ /dev/null @@ -1,801 +0,0 @@ -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))) -} -