fix api server update version
This commit is contained in:
parent
97d6c8f499
commit
08d1cf7069
2
go.mod
2
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue