feat: Implement public access for tenant content
- Add TenantOptionalAuth middleware to allow access to public content without requiring authentication. - Introduce ListPublicPublished and PublicDetail methods in the content service to retrieve publicly accessible content. - Create tenant_public HTTP routes for listing and showing public content, including preview and main asset retrieval. - Enhance content tests to cover scenarios for public content access and permissions. - Update specifications to reflect the new public content access features and rules.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/tenant/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/database"
|
||||
@@ -257,6 +258,117 @@ func (s *content) ListPublished(ctx context.Context, tenantID, userID int64, fil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListPublicPublished 返回“公开可见”的已发布内容列表(给游客/非成员使用)。
|
||||
// 规则:仅返回 published + visibility=public;tenant_only/private 永不通过公开接口暴露。
|
||||
func (s *content) ListPublicPublished(ctx context.Context, tenantID, viewerUserID int64, filter *dto.ContentListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &dto.ContentListFilter{}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"user_id": viewerUserID,
|
||||
"page": filter.Page,
|
||||
"limit": filter.Limit,
|
||||
}).Info("services.content.list_public_published")
|
||||
|
||||
tbl, query := models.ContentQuery.QueryContext(ctx)
|
||||
|
||||
conds := []gen.Condition{
|
||||
tbl.TenantID.Eq(tenantID),
|
||||
tbl.Status.Eq(consts.ContentStatusPublished),
|
||||
tbl.Visibility.Eq(consts.ContentVisibilityPublic),
|
||||
tbl.DeletedAt.IsNull(),
|
||||
}
|
||||
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 := map[int64]bool{}
|
||||
if viewerUserID > 0 {
|
||||
m, err := s.accessSet(ctx, tenantID, viewerUserID, contentIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessSet = m
|
||||
}
|
||||
|
||||
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 == viewerUserID
|
||||
return &dto.ContentItem{
|
||||
Content: model,
|
||||
Price: price,
|
||||
HasAccess: has,
|
||||
}
|
||||
})
|
||||
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: respItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PublicDetail 返回“公开可见”的内容详情(给游客/非成员使用)。
|
||||
// 规则:仅允许 published + visibility=public;否则统一返回 not found,避免信息泄露。
|
||||
func (s *content) PublicDetail(ctx context.Context, tenantID, viewerUserID, contentID int64) (*ContentDetailResult, error) {
|
||||
log.WithFields(log.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"user_id": viewerUserID,
|
||||
"content_id": contentID,
|
||||
}).Info("services.content.public_detail")
|
||||
|
||||
tbl, query := models.ContentQuery.QueryContext(ctx)
|
||||
model, err := query.Where(
|
||||
tbl.TenantID.Eq(tenantID),
|
||||
tbl.ID.Eq(contentID),
|
||||
tbl.DeletedAt.IsNull(),
|
||||
).First()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrRecordNotFound.WithMsg("content not found")
|
||||
}
|
||||
|
||||
// Public endpoints only expose published + public contents.
|
||||
if model.Status != consts.ContentStatusPublished || model.Visibility != consts.ContentVisibilityPublic {
|
||||
return nil, errorx.ErrRecordNotFound.WithMsg("content not found")
|
||||
}
|
||||
|
||||
price, err := s.contentPrice(ctx, tenantID, contentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
free := price == nil || price.PriceAmount == 0
|
||||
|
||||
hasAccess := model.UserID == viewerUserID || free
|
||||
if !hasAccess && viewerUserID > 0 {
|
||||
ok, err := s.HasAccess(ctx, tenantID, viewerUserID, contentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasAccess = ok
|
||||
}
|
||||
|
||||
return &ContentDetailResult{
|
||||
Content: model,
|
||||
Price: price,
|
||||
HasAccess: hasAccess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *content) Detail(ctx context.Context, tenantID, userID, contentID int64) (*ContentDetailResult, error) {
|
||||
log.WithFields(log.Fields{
|
||||
"tenant_id": tenantID,
|
||||
|
||||
Reference in New Issue
Block a user