From 84c8d1b3946bcbda5935dee73a93b24062af2452 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 13 Jan 2026 11:06:32 +0800 Subject: [PATCH] feat: refine content media access rules --- backend/app/services/content.go | 27 +++++++++++++-- backend/app/services/content_test.go | 49 +++++++++++++++++++++++++++- docs/todo_list.md | 1 + 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/backend/app/services/content.go b/backend/app/services/content.go index 5f1405b..5627823 100644 --- a/backend/app/services/content.go +++ b/backend/app/services/content.go @@ -204,16 +204,39 @@ func (s *content) Get(ctx context.Context, tenantID, userID, id int64) (*content if item.UserID == uid { hasAccess = true // Owner } else { + isAdmin, err := s.isTenantAdmin(ctx, item.TenantID, uid) + if err != nil { + return nil, err + } + if isAdmin { + hasAccess = true + } + } + + if !hasAccess { // Check Purchase - exists, _ := models.ContentAccessQuery.WithContext(ctx). + exists, err := models.ContentAccessQuery.WithContext(ctx). Where(models.ContentAccessQuery.UserID.Eq(uid), models.ContentAccessQuery.ContentID.Eq(id), models.ContentAccessQuery.Status.Eq(consts.ContentAccessStatusActive)). Exists() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } if exists { hasAccess = true } } + + if !hasAccess && item.Visibility == consts.ContentVisibilityTenantOnly { + isMember, err := s.isTenantMember(ctx, item.TenantID, uid) + if err != nil { + return nil, err + } + if isMember { + hasAccess = true + } + } } // Filter Assets based on Access @@ -250,7 +273,7 @@ func (s *content) Get(ctx context.Context, tenantID, userID, id int64) (*content IsFavorited: isFavorited, } // Pass IsPurchased/HasAccess info to frontend? - detail.ContentItem.IsPurchased = hasAccess // Update DTO field logic if needed. IsPurchased usually means "Bought". Owner implies access but not necessarily purchased. But for UI "Play" button, IsPurchased=true is fine. + detail.ContentItem.IsPurchased = hasAccess // 购买或具备可访问权限时统一展示为已解锁。 return detail, nil } diff --git a/backend/app/services/content_test.go b/backend/app/services/content_test.go index 237b349..42cad44 100644 --- a/backend/app/services/content_test.go +++ b/backend/app/services/content_test.go @@ -116,14 +116,21 @@ func (s *ContentTestSuite) Test_Get() { models.ContentQuery.WithContext(ctx).Create(content) member := &models.User{Nickname: "Member", Username: "member1", Phone: "13800000003"} + admin := &models.User{Nickname: "Admin", Username: "admin1", Phone: "13800000005"} guest := &models.User{Nickname: "Guest", Username: "guest1", Phone: "13800000004"} - models.UserQuery.WithContext(ctx).Create(member, guest) + models.UserQuery.WithContext(ctx).Create(member, admin, guest) models.TenantUserQuery.WithContext(ctx).Create(&models.TenantUser{ TenantID: 1, UserID: member.ID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}, Status: consts.UserStatusVerified, }) + models.TenantUserQuery.WithContext(ctx).Create(&models.TenantUser{ + TenantID: 1, + UserID: admin.ID, + Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleTenantAdmin}, + Status: consts.UserStatusVerified, + }) tenantOnly := &models.Content{ TenantID: 1, @@ -152,6 +159,38 @@ func (s *ContentTestSuite) Test_Get() { } models.ContentAssetQuery.WithContext(ctx).Create(ca) + tenantMain := &models.MediaAsset{ + TenantID: 1, + UserID: author.ID, + ObjectKey: "tenant_main.mp4", + Type: consts.MediaAssetTypeVideo, + } + tenantPrev := &models.MediaAsset{ + TenantID: 1, + UserID: author.ID, + ObjectKey: "tenant_preview.mp4", + Type: consts.MediaAssetTypeVideo, + } + models.MediaAssetQuery.WithContext(ctx).Create(tenantMain, tenantPrev) + models.ContentAssetQuery.WithContext(ctx).Create( + &models.ContentAsset{ + TenantID: 1, + UserID: author.ID, + ContentID: tenantOnly.ID, + AssetID: tenantMain.ID, + Sort: 1, + Role: consts.ContentAssetRoleMain, + }, + &models.ContentAsset{ + TenantID: 1, + UserID: author.ID, + ContentID: tenantOnly.ID, + AssetID: tenantPrev.ID, + Sort: 2, + Role: consts.ContentAssetRolePreview, + }, + ) + // Set context to author ctx = context.WithValue(ctx, consts.CtxKeyUser, author.ID) @@ -168,6 +207,14 @@ func (s *ContentTestSuite) Test_Get() { detail, err := Content.Get(ctx, tenantID, member.ID, tenantOnly.ID) So(err, ShouldBeNil) So(detail.Title, ShouldEqual, "Member Only") + So(len(detail.MediaUrls), ShouldEqual, 2) + }) + + Convey("should allow tenant_only content for admin", func() { + detail, err := Content.Get(ctx, tenantID, admin.ID, tenantOnly.ID) + So(err, ShouldBeNil) + So(detail.Title, ShouldEqual, "Member Only") + So(len(detail.MediaUrls), ShouldEqual, 2) }) Convey("should reject tenant_only content for non-member", func() { diff --git a/docs/todo_list.md b/docs/todo_list.md index fc91fa9..498f940 100644 --- a/docs/todo_list.md +++ b/docs/todo_list.md @@ -194,6 +194,7 @@ - 内容可见性与 tenant_only 访问控制。 - OTP 登录流程与租户成员校验(未加入拒绝登录)。 - ID 类型已统一为 int64(仅保留 upload_id/external_id/uuid 等非数字标识)。 +- 内容资源权限与预览差异化(未购预览、已购/管理员/成员全量)。 ## 里程碑建议 - M1:完成 P0