Merge pull request #6812 from Pluviobyte/codex/fix-model-plaza-subscriptions

fix(model-plaza): include active subscription groups
This commit is contained in:
Wesley Liddick
2026-09-08 20:10:42 +08:00
committed by GitHub
4 changed files with 126 additions and 5 deletions
@@ -15,7 +15,7 @@ import (
// 广场路由挂 OptionalJWT 中间件:匿名可访问(除非 require_auth 开启),带 token 则
// 识别用户。可见性规则(橱窗语义,与「可用渠道」的可绑定语义不同):
// - 匿名:仅非专属分组(订阅型照常展示);
// - 登录:非专属分组 + user_allowed_groups 授权的专属分组(不检查订阅有效性);
// - 登录:非专属分组 + user_allowed_groups 授权或持有有效订阅的专属分组;
// 若该用户开启了公开分组限制,则公开分组同样需要落在授权集合内。
type ModelPlazaHandler struct {
plazaService *service.ModelPlazaService
@@ -160,7 +160,7 @@ func (h *ModelPlazaHandler) Get(c *gin.Context) {
}
// filterPlazaVisibleGroups 按登录态裁剪分组可见性。
// allowedGroups == nil 表示匿名(仅非专属);非 nil 表示登录(非专属 + 授权专属)。
// allowedGroups == nil 表示匿名(仅非专属);非 nil 包含普通授权及有效订阅分组。
// restrictPublicGroups 为 true 时,公开分组也必须落在 allowedGroups 内,否则用户会
// 在广场看到自己实际绑定不了的分组。
func filterPlazaVisibleGroups(
@@ -244,3 +244,17 @@ func TestToModelPlazaGroupDTO_TimePricing(t *testing.T) {
weekdaysTP := weekdaysModel["time_pricing"].(map[string]any)
require.Equal(t, true, weekdaysTP["weekdays_only"])
}
func TestFilterPlazaVisibleGroups_SubscribedExclusiveGroup(t *testing.T) {
groups := []service.PlazaGroup{
{ID: 42, IsExclusive: true, SubscriptionType: "subscription"},
{ID: 43, IsExclusive: true, SubscriptionType: "subscription"},
{ID: 44, IsExclusive: true, SubscriptionType: "standard"},
}
require.Empty(t, filterPlazaVisibleGroups(groups, nil, false))
for _, restricted := range []bool{false, true} {
visible := filterPlazaVisibleGroups(groups, map[int64]struct{}{42: {}}, restricted)
require.Len(t, visible, 1)
require.Equal(t, int64(42), visible[0].ID)
}
}
@@ -0,0 +1,100 @@
//go:build unit
package service
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type visibilityUserRepo struct {
UserRepository
user *User
err error
}
func (r *visibilityUserRepo) GetByID(context.Context, int64) (*User, error) { return r.user, r.err }
type visibilitySubRepo struct {
UserSubscriptionRepository
subscriptions []UserSubscription
err error
calls int
}
func (r *visibilitySubRepo) ListActiveByUserID(_ context.Context, userID int64) ([]UserSubscription, error) {
r.calls++
// Match the repository's active-status and expiry predicates.
active := make([]UserSubscription, 0)
for _, sub := range r.subscriptions {
if sub.UserID == userID && sub.IsActive() {
active = append(active, sub)
}
}
return active, r.err
}
type visibilityGroupRepo struct {
GroupRepository
groups []Group
}
func (r *visibilityGroupRepo) ListActive(context.Context) ([]Group, error) { return r.groups, nil }
func TestGetUserGroupVisibilityIncludesActiveSubscriptions(t *testing.T) {
for _, restricted := range []bool{false, true} {
t.Run(map[bool]string{false: "unrestricted", true: "restricted"}[restricted], func(t *testing.T) {
now := time.Now()
subs := &visibilitySubRepo{subscriptions: []UserSubscription{
{UserID: 1, GroupID: 42, Status: SubscriptionStatusActive, ExpiresAt: now.Add(time.Hour)},
{UserID: 1, GroupID: 43, Status: SubscriptionStatusActive, ExpiresAt: now.Add(-time.Hour)},
{UserID: 1, GroupID: 44, Status: "expired", ExpiresAt: now.Add(time.Hour)},
{UserID: 2, GroupID: 45, Status: SubscriptionStatusActive, ExpiresAt: now.Add(time.Hour)},
}}
svc := &APIKeyService{
userRepo: &visibilityUserRepo{user: &User{ID: 1, AllowedGroups: []int64{7}, RestrictPublicGroups: restricted}},
userSubRepo: subs,
groupRepo: &visibilityGroupRepo{groups: []Group{{ID: 42, IsExclusive: true, SubscriptionType: "subscription"}}},
}
available, err := svc.GetAvailableGroups(context.Background(), 1)
require.NoError(t, err)
require.Len(t, available, 1)
visible, restrict, err := svc.GetUserGroupVisibility(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, restricted, restrict)
require.Equal(t, map[int64]struct{}{7: {}, 42: {}}, visible)
require.Contains(t, visible, available[0].ID, "a subscribed group that can be bound must be visible")
})
}
}
func TestGetUserGroupVisibilityEmptyAndErrors(t *testing.T) {
failure := errors.New("repository unavailable")
for _, tc := range []struct {
name string
userErr, subErr error
}{
{name: "empty"}, {name: "user failure", userErr: failure}, {name: "subscription failure", subErr: failure},
} {
t.Run(tc.name, func(t *testing.T) {
subs := &visibilitySubRepo{err: tc.subErr}
svc := &APIKeyService{userRepo: &visibilityUserRepo{user: &User{ID: 1}, err: tc.userErr}, userSubRepo: subs}
got, _, err := svc.GetUserGroupVisibility(context.Background(), 1)
if tc.userErr != nil || tc.subErr != nil {
require.ErrorIs(t, err, failure)
require.Nil(t, got, "repository failures must not become anonymous visibility")
} else {
require.NoError(t, err)
require.NotNil(t, got, "an empty logged-in user must not become anonymous")
require.Empty(t, got)
}
if tc.userErr != nil {
require.Zero(t, subs.calls)
}
})
}
}
+10 -3
View File
@@ -1071,11 +1071,11 @@ func (s *APIKeyService) SearchAPIKeys(ctx context.Context, userID int64, keyword
return keys, nil
}
// GetUserGroupVisibility 返回 user_allowed_groups 授权给该用户的分组 ID 集合,
// GetUserGroupVisibility 返回 user_allowed_groups 授权及有效订阅的分组 ID 集合,
// 以及该用户是否开启了公开分组限制。开启时公开分组的可见性也要落在该集合内。
//
// 与 GetAvailableGroups 的区别:这里是「橱窗」语义(模型广场用),不检查订阅有效性,
// 也不关心分组是否活跃——仅回答"哪些专属分组对该用户可见"。返回值恒非 nil。
// 与 GetAvailableGroups 的区别:这里保留普通授权分组的「橱窗」语义,不检查
// 分组是否活跃;有效订阅也授予对应分组的可见性。返回值恒非 nil。
func (s *APIKeyService) GetUserGroupVisibility(ctx context.Context, userID int64) (map[int64]struct{}, bool, error) {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
@@ -1085,6 +1085,13 @@ func (s *APIKeyService) GetUserGroupVisibility(ctx context.Context, userID int64
for _, id := range user.AllowedGroups {
allowed[id] = struct{}{}
}
subscriptions, err := s.userSubRepo.ListActiveByUserID(ctx, userID)
if err != nil {
return nil, false, fmt.Errorf("list active subscriptions: %w", err)
}
for _, sub := range subscriptions {
allowed[sub.GroupID] = struct{}{}
}
return allowed, user.RestrictPublicGroups, nil
}