package services import ( "context" "errors" "quyun/v2/app/errorx" content_dto "quyun/v2/app/http/v1/dto" user_dto "quyun/v2/app/http/v1/dto" "quyun/v2/app/requests" "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/spf13/cast" "gorm.io/gorm" ) // @provider type content struct{} func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilter) (*requests.Pager, error) { tbl, q := models.ContentQuery.QueryContext(ctx) // Filters q = q.Where(tbl.Status.Eq(consts.ContentStatusPublished)) if filter.Keyword != "" { q = q.Where(tbl.Title.Like("%" + filter.Keyword + "%")) } if filter.Genre != "" { q = q.Where(tbl.Genre.Eq(filter.Genre)) } if filter.TenantID != "" { tid := cast.ToInt64(filter.TenantID) q = q.Where(tbl.TenantID.Eq(tid)) } // Preload Author q = q.Preload(tbl.Author) // Sort switch filter.Sort { case "hot": q = q.Order(tbl.Views.Desc()) case "price_asc": q = q.Order(tbl.ID.Desc()) default: // latest q = q.Order(tbl.PublishedAt.Desc()) } // Pagination // Use embedded pagination directly filter.Pagination.Format() total, err := q.Count() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } // Convert to DTO data := make([]content_dto.ContentItem, len(list)) for i, item := range list { data[i] = s.toContentItemDTO(item) } return &requests.Pager{ Pagination: filter.Pagination, Total: total, Items: data, }, nil } func (s *content) Get(ctx context.Context, id string) (*content_dto.ContentDetail, error) { cid := cast.ToInt64(id) _, q := models.ContentQuery.QueryContext(ctx) var item models.Content // Use UnderlyingDB for complex nested preloading err := q.UnderlyingDB(). Preload("Author"). Preload("ContentAssets", func(db *gorm.DB) *gorm.DB { return db.Order("sort ASC") }). Preload("ContentAssets.Asset"). Where("id = ?", cid). First(&item).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound } return nil, errorx.ErrDatabaseError.WithCause(err) } // Interaction status (isLiked, isFavorited) userID := ctx.Value(consts.CtxKeyUser) isLiked := false isFavorited := false if userID != nil { // uid := cast.ToInt64(userID) // Unused for now until interaction query implemented // ... check likes ... } detail := &content_dto.ContentDetail{ ContentItem: s.toContentItemDTO(&item), Description: item.Description, Body: item.Body, MediaUrls: s.toMediaURLs(item.ContentAssets), IsLiked: isLiked, IsFavorited: isFavorited, } return detail, nil } func (s *content) ListComments(ctx context.Context, id string, page int) (*requests.Pager, error) { cid := cast.ToInt64(id) tbl, q := models.CommentQuery.QueryContext(ctx) q = q.Where(tbl.ContentID.Eq(cid)).Preload(tbl.User) q = q.Order(tbl.CreatedAt.Desc()) p := requests.Pagination{Page: int64(page), Limit: 10} total, err := q.Count() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } data := make([]content_dto.Comment, len(list)) for i, v := range list { data[i] = content_dto.Comment{ ID: cast.ToString(v.ID), Content: v.Content, UserID: cast.ToString(v.UserID), UserNickname: v.User.Nickname, // Preloaded UserAvatar: v.User.Avatar, CreateTime: v.CreatedAt.Format("2006-01-02 15:04:05"), Likes: int(v.Likes), ReplyTo: cast.ToString(v.ReplyTo), } } return &requests.Pager{ Pagination: requests.Pagination{ Page: p.Page, Limit: p.Limit, }, Total: total, Items: data, }, nil } func (s *content) CreateComment(ctx context.Context, id string, form *content_dto.CommentCreateForm) error { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return errorx.ErrUnauthorized } uid := cast.ToInt64(userID) cid := cast.ToInt64(id) c, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid)).First() if err != nil { return errorx.ErrRecordNotFound } comment := &models.Comment{ TenantID: c.TenantID, UserID: uid, ContentID: cid, Content: form.Content, ReplyTo: cast.ToInt64(form.ReplyTo), } if err := models.CommentQuery.WithContext(ctx).Create(comment); err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil } func (s *content) LikeComment(ctx context.Context, id string) error { return nil } func (s *content) GetLibrary(ctx context.Context) ([]user_dto.ContentItem, error) { return []user_dto.ContentItem{}, nil } func (s *content) GetFavorites(ctx context.Context) ([]user_dto.ContentItem, error) { return []user_dto.ContentItem{}, nil } func (s *content) AddFavorite(ctx context.Context, contentId string) error { return nil } func (s *content) RemoveFavorite(ctx context.Context, contentId string) error { return nil } func (s *content) GetLikes(ctx context.Context) ([]user_dto.ContentItem, error) { return []user_dto.ContentItem{}, nil } func (s *content) AddLike(ctx context.Context, contentId string) error { return nil } func (s *content) RemoveLike(ctx context.Context, contentId string) error { return nil } func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) { return []content_dto.Topic{}, nil } // Helpers func (s *content) toContentItemDTO(item *models.Content) content_dto.ContentItem { dto := content_dto.ContentItem{ ID: cast.ToString(item.ID), Title: item.Title, Genre: item.Genre, AuthorID: cast.ToString(item.UserID), Views: int(item.Views), Likes: int(item.Likes), } if item.Author != nil { dto.AuthorName = item.Author.Nickname dto.AuthorAvatar = item.Author.Avatar } return dto } func (s *content) toMediaURLs(assets []*models.ContentAsset) []content_dto.MediaURL { var urls []content_dto.MediaURL for _, ca := range assets { if ca.Asset != nil { // Construct URL based on Asset info (Bucket/Key/Provider) // For prototype: mock url url := "http://mock/" + ca.Asset.ObjectKey urls = append(urls, content_dto.MediaURL{ Type: string(ca.Asset.Type), // Assuming type is enum or string URL: url, }) } } return urls }