backend/pkg/post/repository/comment.go

384 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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