backend/pkg/post/repository/comment.go

384 lines
10 KiB
Go
Raw Normal View History

2025-11-19 09:06:44 +00:00
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)
}