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