package services import ( "context" "errors" "time" "quyun/v2/app/http/tenant/dto" "quyun/v2/app/requests" "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" pkgerrors "github.com/pkg/errors" "github.com/samber/lo" log "github.com/sirupsen/logrus" "go.ipao.vip/gen" "gorm.io/gorm" ) // content 实现内容域相关的业务能力(创建/更新/定价/授权等)。 // // @provider type content struct{} // ContentDetailResult 为内容详情的内部结果(供 controller 组合返回)。 type ContentDetailResult struct { // Content 内容实体。 Content *models.Content // Price 定价信息(可能为 nil,表示未设置价格)。 Price *models.ContentPrice // HasAccess 当前用户是否拥有主资源访问权限。 HasAccess bool } func (s *content) Create(ctx context.Context, tenantID, userID int64, form *dto.ContentCreateForm) (*models.Content, error) { log.WithFields(log.Fields{ "tenant_id": tenantID, "user_id": userID, }).Info("services.content.create") // 关键默认值:未传可见性时默认“租户内可见”。 visibility := form.Visibility if visibility == "" { visibility = consts.ContentVisibilityTenantOnly } // 试看策略:默认固定时长;并强制不允许下载。 previewSeconds := consts.DefaultContentPreviewSeconds if form.PreviewSeconds != nil && *form.PreviewSeconds > 0 { previewSeconds = *form.PreviewSeconds } m := &models.Content{ TenantID: tenantID, UserID: userID, Title: form.Title, Description: form.Description, Status: consts.ContentStatusDraft, Visibility: visibility, PreviewSeconds: previewSeconds, PreviewDownloadable: false, } if err := m.Create(ctx); err != nil { return nil, pkgerrors.Wrap(err, "create content failed") } return m, nil } func (s *content) Update(ctx context.Context, tenantID, userID, contentID int64, form *dto.ContentUpdateForm) (*models.Content, error) { log.WithFields(log.Fields{ "tenant_id": tenantID, "user_id": userID, "content_id": contentID, }).Info("services.content.update") tbl, query := models.ContentQuery.QueryContext(ctx) m, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.ID.Eq(contentID), ).First() if err != nil { return nil, pkgerrors.Wrap(err, "content not found") } if form.Title != nil { m.Title = *form.Title } if form.Description != nil { m.Description = *form.Description } if form.Visibility != nil { m.Visibility = *form.Visibility } if form.PreviewSeconds != nil && *form.PreviewSeconds > 0 { m.PreviewSeconds = *form.PreviewSeconds m.PreviewDownloadable = false } if form.Status != nil { m.Status = *form.Status // 发布动作:首次发布时补齐发布时间,便于后续排序/检索与审计。 if m.Status == consts.ContentStatusPublished && m.PublishedAt.IsZero() { m.PublishedAt = time.Now() } } if _, err := m.Update(ctx); err != nil { return nil, pkgerrors.Wrap(err, "update content failed") } return m, nil } func (s *content) UpsertPrice(ctx context.Context, tenantID, userID, contentID int64, form *dto.ContentPriceUpsertForm) (*models.ContentPrice, error) { log.WithFields(log.Fields{ "tenant_id": tenantID, "user_id": userID, "content_id": contentID, "amount": form.PriceAmount, }).Info("services.content.upsert_price") currency := form.Currency if currency == "" { currency = consts.CurrencyCNY } discountType := form.DiscountType if discountType == "" { discountType = consts.DiscountTypeNone } startAt := time.Time{} if form.DiscountStartAt != nil { startAt = *form.DiscountStartAt } endAt := time.Time{} if form.DiscountEndAt != nil { endAt = *form.DiscountEndAt } tbl, query := models.ContentPriceQuery.QueryContext(ctx) m, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.ContentID.Eq(contentID), ).First() if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, pkgerrors.Wrap(err, "find content price failed") } if errors.Is(err, gorm.ErrRecordNotFound) { m = &models.ContentPrice{ TenantID: tenantID, UserID: userID, ContentID: contentID, Currency: currency, PriceAmount: form.PriceAmount, DiscountType: discountType, DiscountValue: form.DiscountValue, DiscountStartAt: startAt, DiscountEndAt: endAt, } if err := m.Create(ctx); err != nil { return nil, pkgerrors.Wrap(err, "create content price failed") } return m, nil } m.UserID = userID m.Currency = currency m.PriceAmount = form.PriceAmount m.DiscountType = discountType m.DiscountValue = form.DiscountValue m.DiscountStartAt = startAt m.DiscountEndAt = endAt if _, err := m.Update(ctx); err != nil { return nil, pkgerrors.Wrap(err, "update content price failed") } return m, nil } func (s *content) AttachAsset(ctx context.Context, tenantID, userID, contentID, assetID int64, role consts.ContentAssetRole, sort int32, now time.Time) (*models.ContentAsset, error) { log.WithFields(log.Fields{ "tenant_id": tenantID, "user_id": userID, "content_id": contentID, "asset_id": assetID, "role": role, "sort": sort, }).Info("services.content.attach_asset") m := &models.ContentAsset{ TenantID: tenantID, UserID: userID, ContentID: contentID, AssetID: assetID, Role: role, Sort: sort, CreatedAt: now, UpdatedAt: now, } if err := m.Create(ctx); err != nil { return nil, pkgerrors.Wrap(err, "attach content asset failed") } return m, nil } func (s *content) ListPublished(ctx context.Context, tenantID, userID int64, filter *dto.ContentListFilter) (*requests.Pager, error) { log.WithFields(log.Fields{ "tenant_id": tenantID, "user_id": userID, "page": filter.Page, "limit": filter.Limit, }).Info("services.content.list_published") tbl, query := models.ContentQuery.QueryContext(ctx) conds := []gen.Condition{ tbl.TenantID.Eq(tenantID), tbl.Status.Eq(consts.ContentStatusPublished), tbl.Visibility.In(consts.ContentVisibilityPublic, consts.ContentVisibilityTenantOnly), } if filter.Keyword != nil && *filter.Keyword != "" { conds = append(conds, tbl.Title.Like(database.WrapLike(*filter.Keyword))) } filter.Pagination.Format() items, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit)) if err != nil { return nil, err } contentIDs := lo.Map(items, func(item *models.Content, _ int) int64 { return item.ID }) priceByContent, err := s.contentPriceMapping(ctx, tenantID, contentIDs) if err != nil { return nil, err } accessSet, err := s.accessSet(ctx, tenantID, userID, contentIDs) if err != nil { return nil, err } respItems := lo.Map(items, func(model *models.Content, _ int) *dto.ContentItem { price := priceByContent[model.ID] free := price == nil || price.PriceAmount == 0 has := free || accessSet[model.ID] || model.UserID == userID return &dto.ContentItem{ Content: model, Price: price, HasAccess: has, } }) return &requests.Pager{ Pagination: filter.Pagination, Total: total, Items: respItems, }, nil } func (s *content) Detail(ctx context.Context, tenantID, userID, contentID int64) (*ContentDetailResult, error) { log.WithFields(log.Fields{ "tenant_id": tenantID, "user_id": userID, "content_id": contentID, }).Info("services.content.detail") tbl, query := models.ContentQuery.QueryContext(ctx) model, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.ID.Eq(contentID), ).First() if err != nil { return nil, pkgerrors.Wrapf(err, "content not found, tenantID=%d, contentID=%d", tenantID, contentID) } if model.Status != consts.ContentStatusPublished && model.UserID != userID { return nil, errors.New("content is not published") } price, err := s.contentPrice(ctx, tenantID, contentID) if err != nil { return nil, err } free := price == nil || price.PriceAmount == 0 canView := false switch model.Visibility { case consts.ContentVisibilityPublic, consts.ContentVisibilityTenantOnly: canView = true case consts.ContentVisibilityPrivate: canView = model.UserID == userID default: canView = false } hasAccess := model.UserID == userID || free if !hasAccess { ok, err := s.HasAccess(ctx, tenantID, userID, contentID) if err != nil { return nil, err } hasAccess = ok canView = canView || ok } if !canView { return nil, errors.New("content is private") } return &ContentDetailResult{ Content: model, Price: price, HasAccess: hasAccess, }, nil } func (s *content) HasAccess(ctx context.Context, tenantID, userID, contentID int64) (bool, error) { log.WithFields(log.Fields{ "tenant_id": tenantID, "user_id": userID, "content_id": contentID, }).Info("services.content.has_access") tbl, query := models.ContentAccessQuery.QueryContext(ctx) _, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID), tbl.ContentID.Eq(contentID), tbl.Status.Eq(consts.ContentAccessStatusActive), ).First() if err != nil { return false, nil } return true, nil } func (s *content) AssetsByRole(ctx context.Context, tenantID, contentID int64, role consts.ContentAssetRole) ([]*models.MediaAsset, error) { log.WithFields(log.Fields{ "tenant_id": tenantID, "content_id": contentID, "role": role, }).Info("services.content.assets_by_role") maTbl, maQuery := models.MediaAssetQuery.QueryContext(ctx) caTbl, _ := models.ContentAssetQuery.QueryContext(ctx) assets, err := maQuery. LeftJoin(caTbl, caTbl.AssetID.EqCol(maTbl.ID)). Select(maTbl.ALL). Where( maTbl.TenantID.Eq(tenantID), caTbl.TenantID.Eq(tenantID), caTbl.ContentID.Eq(contentID), caTbl.Role.Eq(role), ). Order(caTbl.Sort.Asc()). Find() if err != nil { return nil, err } return assets, nil } func (s *content) contentPrice(ctx context.Context, tenantID, contentID int64) (*models.ContentPrice, error) { tbl, query := models.ContentPriceQuery.QueryContext(ctx) m, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.ContentID.Eq(contentID), ).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } return m, nil } func (s *content) contentPriceMapping(ctx context.Context, tenantID int64, contentIDs []int64) (map[int64]*models.ContentPrice, error) { if len(contentIDs) == 0 { return map[int64]*models.ContentPrice{}, nil } tbl, query := models.ContentPriceQuery.QueryContext(ctx) items, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.ContentID.In(contentIDs...), ).Find() if err != nil { return nil, err } return lo.SliceToMap(items, func(item *models.ContentPrice) (int64, *models.ContentPrice) { return item.ContentID, item }), nil } func (s *content) accessSet(ctx context.Context, tenantID, userID int64, contentIDs []int64) (map[int64]bool, error) { if len(contentIDs) == 0 { return map[int64]bool{}, nil } tbl, query := models.ContentAccessQuery.QueryContext(ctx) items, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID), tbl.ContentID.In(contentIDs...), tbl.Status.Eq(consts.ContentAccessStatusActive), ).Find() if err != nil { return nil, err } out := make(map[int64]bool, len(items)) for _, item := range items { out[item.ContentID] = true } return out, nil }