Files
quyun-v2/backend/app/services/content.go
2025-12-18 09:54:29 +08:00

417 lines
11 KiB
Go

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 implements content-related domain operations.
//
// @provider
type content struct{}
// ContentDetailResult is the internal detail result used by controllers.
type ContentDetailResult struct {
// Content is the content entity.
Content *models.Content
// Price is the price settings (may be nil).
Price *models.ContentPrice
// HasAccess indicates whether the user can access main assets.
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 := int32(60)
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
}