package logic import ( "context" "database/sql" "time" "git.cialloo.com/CiallooWeb/Blog/app/internal/svc" "git.cialloo.com/CiallooWeb/Blog/app/internal/types" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/zeromicro/go-zero/core/logx" ) type ListPostsLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } // Get a list of blog posts func NewListPostsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListPostsLogic { return &ListPostsLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *ListPostsLogic) ListPosts(req *types.ListPostsReq) (resp *types.ListPostsResp, err error) { // Set default values page := req.Page if page < 1 { page = 1 } pageSize := req.PageSize if pageSize < 1 || pageSize > 100 { pageSize = 10 // Default page size } offset := (page - 1) * pageSize // Build query with optional tag filter var countQuery string var postsQuery string var args []interface{} if len(req.TagIds) > 0 { // Filter by tags - posts that have ALL specified tags countQuery = ` SELECT COUNT(DISTINCT p.id) FROM posts p INNER JOIN post_hashtags ph ON p.id = ph.post_id WHERE ph.hashtag_id = ANY($1) GROUP BY p.id HAVING COUNT(DISTINCT ph.hashtag_id) = $2 ` postsQuery = ` SELECT DISTINCT p.id, p.title, p.created_at, p.updated_at, p.cover_id FROM posts p INNER JOIN post_hashtags ph ON p.id = ph.post_id WHERE ph.hashtag_id = ANY($1) GROUP BY p.id, p.title, p.created_at, p.updated_at, p.cover_id HAVING COUNT(DISTINCT ph.hashtag_id) = $2 ORDER BY p.created_at DESC LIMIT $3 OFFSET $4 ` args = []interface{}{req.TagIds, len(req.TagIds), pageSize, offset} } else { // No tag filter - get all posts countQuery = `SELECT COUNT(*) FROM posts` postsQuery = ` SELECT p.id, p.title, p.created_at, p.updated_at, p.cover_id FROM posts p ORDER BY p.created_at DESC LIMIT $1 OFFSET $2 ` args = []interface{}{pageSize, offset} } // Get total count var totalCount int if len(req.TagIds) > 0 { // Count posts matching tag filter rows, err := l.svcCtx.DB.QueryContext(l.ctx, countQuery, req.TagIds, len(req.TagIds)) if err != nil { l.Errorf("Failed to get total count: %v", err) return nil, err } defer rows.Close() totalCount = 0 for rows.Next() { totalCount++ } } else { err = l.svcCtx.DB.QueryRowContext(l.ctx, countQuery).Scan(&totalCount) if err != nil { l.Errorf("Failed to get total count: %v", err) return nil, err } } // Get posts with pagination rows, err := l.svcCtx.DB.QueryContext(l.ctx, postsQuery, args...) if err != nil { l.Errorf("Failed to get posts: %v", err) return nil, err } defer rows.Close() posts := []types.ListPostsRespPosts{} for rows.Next() { var post types.ListPostsRespPosts var coverID sql.NullInt64 var createdAt, updatedAt time.Time err := rows.Scan(&post.PostId, &post.Title, &createdAt, &updatedAt, &coverID) if err != nil { l.Errorf("Failed to scan post: %v", err) continue } // Convert timestamps to Unix milliseconds post.CreatedAt = createdAt.UnixMilli() post.UpdatedAt = updatedAt.UnixMilli() // If cover image exists, get its URL if coverID.Valid { coverQuery := `SELECT file_key FROM files WHERE id = $1` var fileKey string err = l.svcCtx.DB.QueryRowContext(l.ctx, coverQuery, coverID.Int64).Scan(&fileKey) if err == nil { // Generate presigned URL for cover image expiration := time.Duration(l.svcCtx.Config.S3.PresignedURLExpiration) * time.Second presignClient := s3.NewPresignClient(l.svcCtx.S3Client) getObjectInput := &s3.GetObjectInput{ Bucket: &l.svcCtx.Config.S3.Bucket, Key: &fileKey, } presignedReq, err := presignClient.PresignGetObject(l.ctx, getObjectInput, func(opts *s3.PresignOptions) { opts.Expires = expiration }) if err == nil { post.CoverImageUrl = presignedReq.URL } } } posts = append(posts, post) } return &types.ListPostsResp{ Posts: posts, TotalCount: totalCount, }, nil }