mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
Merge pull request #6812 from Pluviobyte/codex/fix-model-plaza-subscriptions
fix(model-plaza): include active subscription groups
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user