384 lines
10 KiB
Go
384 lines
10 KiB
Go
|
|
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)
|
|||
|
|
}
|