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 != nil && *filter.Keyword != "" { q = q.Where(tbl.Title.Like("%" + *filter.Keyword + "%")) } if filter.Genre != nil && *filter.Genre != "" { q = q.Where(tbl.Genre.Eq(*filter.Genre)) } if filter.TenantID != nil && *filter.TenantID != "" { tid := cast.ToInt64(*filter.TenantID) q = q.Where(tbl.TenantID.Eq(tid)) } // Sort sort := "latest" if filter.Sort != nil && *filter.Sort != "" { sort = *filter.Sort } switch 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 filter.Pagination.Format() total, err := q.Count() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } // Use UnderlyingDB for complex preloads var list []*models.Content err = q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)). UnderlyingDB(). Preload("Author"). Preload("ContentAssets.Asset"). Find(&list).Error 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 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 := false isFavorited := false if userID := ctx.Value(consts.CtxKeyUser); userID != nil { uid := cast.ToInt64(userID) isLiked, _ = models.UserContentActionQuery.WithContext(ctx). Where(models.UserContentActionQuery.UserID.Eq(uid), models.UserContentActionQuery.ContentID.Eq(cid), models.UserContentActionQuery.Type.Eq("like")). Exists() isFavorited, _ = models.UserContentActionQuery.WithContext(ctx). Where(models.UserContentActionQuery.UserID.Eq(uid), models.UserContentActionQuery.ContentID.Eq(cid), models.UserContentActionQuery.Type.Eq("favorite")). Exists() } 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) } // User likes likedMap := make(map[int64]bool) if userID := ctx.Value(consts.CtxKeyUser); userID != nil { uid := cast.ToInt64(userID) ids := make([]int64, len(list)) for i, v := range list { ids[i] = v.ID } likes, _ := models.UserCommentActionQuery.WithContext(ctx). Where(models.UserCommentActionQuery.UserID.Eq(uid), models.UserCommentActionQuery.CommentID.In(ids...), models.UserCommentActionQuery.Type.Eq("like")). Find() for _, l := range likes { likedMap[l.CommentID] = true } } 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, UserAvatar: v.User.Avatar, CreateTime: v.CreatedAt.Format("2006-01-02 15:04:05"), Likes: int(v.Likes), ReplyTo: cast.ToString(v.ReplyTo), IsLiked: likedMap[v.ID], } } 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 { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return errorx.ErrUnauthorized } uid := cast.ToInt64(userID) cmid := cast.ToInt64(id) return models.Q.Transaction(func(tx *models.Query) error { exists, _ := tx.UserCommentAction.WithContext(ctx). Where(tx.UserCommentAction.UserID.Eq(uid), tx.UserCommentAction.CommentID.Eq(cmid), tx.UserCommentAction.Type.Eq("like")). Exists() if exists { return nil } action := &models.UserCommentAction{UserID: uid, CommentID: cmid, Type: "like"} if err := tx.UserCommentAction.WithContext(ctx).Create(action); err != nil { return err } _, err := tx.Comment.WithContext(ctx).Where(tx.Comment.ID.Eq(cmid)).UpdateSimple(tx.Comment.Likes.Add(1)) return err }) } func (s *content) GetLibrary(ctx context.Context) ([]user_dto.ContentItem, error) { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return nil, errorx.ErrUnauthorized } uid := cast.ToInt64(userID) tbl, q := models.ContentAccessQuery.QueryContext(ctx) accessList, err := q.Where(tbl.UserID.Eq(uid), tbl.Status.Eq(consts.ContentAccessStatusActive)).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } if len(accessList) == 0 { return []user_dto.ContentItem{}, nil } var contentIDs []int64 for _, a := range accessList { contentIDs = append(contentIDs, a.ContentID) } ctbl, cq := models.ContentQuery.QueryContext(ctx) var list []*models.Content err = cq.Where(ctbl.ID.In(contentIDs...)). UnderlyingDB(). Preload("Author"). Preload("ContentAssets.Asset"). Find(&list).Error if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var data []user_dto.ContentItem for _, item := range list { dto := s.toContentItemDTO(item) dto.IsPurchased = true data = append(data, dto) } return data, nil } func (s *content) GetFavorites(ctx context.Context) ([]user_dto.ContentItem, error) { return s.getInteractList(ctx, "favorite") } func (s *content) AddFavorite(ctx context.Context, contentId string) error { return s.addInteract(ctx, contentId, "favorite") } func (s *content) RemoveFavorite(ctx context.Context, contentId string) error { return s.removeInteract(ctx, contentId, "favorite") } func (s *content) GetLikes(ctx context.Context) ([]user_dto.ContentItem, error) { return s.getInteractList(ctx, "like") } func (s *content) AddLike(ctx context.Context, contentId string) error { return s.addInteract(ctx, contentId, "like") } func (s *content) RemoveLike(ctx context.Context, contentId string) error { return s.removeInteract(ctx, contentId, "like") } func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) { // topics usually hardcoded or from a dedicated table 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 } // Determine Type and Cover from assets var hasVideo, hasAudio bool for _, asset := range item.ContentAssets { if asset.Asset == nil { continue } // Cover if asset.Role == consts.ContentAssetRoleCover { dto.Cover = "http://mock/" + asset.Asset.ObjectKey } // Type detection switch asset.Asset.Type { case consts.MediaAssetTypeVideo: hasVideo = true case consts.MediaAssetTypeAudio: hasAudio = true } } // Fallback for cover if not explicitly set as cover role (take first image or whatever) if dto.Cover == "" && len(item.ContentAssets) > 0 { for _, asset := range item.ContentAssets { if asset.Asset != nil && asset.Asset.Type == consts.MediaAssetTypeImage { dto.Cover = "http://mock/" + asset.Asset.ObjectKey break } } } if hasVideo { dto.Type = "video" } else if hasAudio { dto.Type = "audio" } else { dto.Type = "article" } 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 { url := "http://mock/" + ca.Asset.ObjectKey urls = append(urls, content_dto.MediaURL{ Type: string(ca.Asset.Type), URL: url, }) } } return urls } func (s *content) addInteract(ctx context.Context, contentId, typ string) error { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return errorx.ErrUnauthorized } uid := cast.ToInt64(userID) cid := cast.ToInt64(contentId) return models.Q.Transaction(func(tx *models.Query) error { exists, _ := tx.UserContentAction.WithContext(ctx). Where(tx.UserContentAction.UserID.Eq(uid), tx.UserContentAction.ContentID.Eq(cid), tx.UserContentAction.Type.Eq(typ)). Exists() if exists { return nil } action := &models.UserContentAction{UserID: uid, ContentID: cid, Type: typ} if err := tx.UserContentAction.WithContext(ctx).Create(action); err != nil { return err } if typ == "like" { _, err := tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid)).UpdateSimple(tx.Content.Likes.Add(1)) return err } return nil }) } func (s *content) removeInteract(ctx context.Context, contentId, typ string) error { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return errorx.ErrUnauthorized } uid := cast.ToInt64(userID) cid := cast.ToInt64(contentId) return models.Q.Transaction(func(tx *models.Query) error { res, err := tx.UserContentAction.WithContext(ctx). Where(tx.UserContentAction.UserID.Eq(uid), tx.UserContentAction.ContentID.Eq(cid), tx.UserContentAction.Type.Eq(typ)). Delete() if err != nil { return err } if res.RowsAffected == 0 { return nil } if typ == "like" { _, err := tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid)).UpdateSimple(tx.Content.Likes.Sub(1)) return err } return nil }) } func (s *content) getInteractList(ctx context.Context, typ string) ([]user_dto.ContentItem, error) { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return nil, errorx.ErrUnauthorized } uid := cast.ToInt64(userID) tbl, q := models.UserContentActionQuery.QueryContext(ctx) actions, err := q.Where(tbl.UserID.Eq(uid), tbl.Type.Eq(typ)).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } if len(actions) == 0 { return []user_dto.ContentItem{}, nil } var contentIDs []int64 for _, a := range actions { contentIDs = append(contentIDs, a.ContentID) } ctbl, cq := models.ContentQuery.QueryContext(ctx) var list []*models.Content err = cq.Where(ctbl.ID.In(contentIDs...)). UnderlyingDB(). Preload("Author"). Preload("ContentAssets.Asset"). Find(&list).Error if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var data []user_dto.ContentItem for _, item := range list { data = append(data, s.toContentItemDTO(item)) } return data, nil }