mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #4358 from DanisJiang/feat/admin-recharge-affiliate-rebate
feat(affiliate): support rebates for admin balance deposits
This commit is contained in:
@@ -176,7 +176,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
dashboardHandler := admin.NewDashboardHandler(dashboardService, dashboardAggregationService)
|
||||
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
|
||||
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
|
||||
adminService := service.NewAdminService(userRepository, groupRepository, adminAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, userRPMCache, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory, openAIGatewayService)
|
||||
adminService := service.NewAdminService(userRepository, groupRepository, adminAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, userRPMCache, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory, openAIGatewayService, affiliateService)
|
||||
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService, serviceUserPlatformQuotaRepository, billingCache)
|
||||
groupCapacityService := service.NewGroupCapacityService(accountRepository, groupRepository, concurrencyService, sessionLimitCache, rpmCache)
|
||||
groupHandler := admin.NewGroupHandler(adminService, dashboardService, groupCapacityService)
|
||||
|
||||
@@ -229,6 +229,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
AffiliateRebateFreezeHours: settings.AffiliateRebateFreezeHours,
|
||||
AffiliateRebateDurationDays: settings.AffiliateRebateDurationDays,
|
||||
AffiliateRebatePerInviteeCap: settings.AffiliateRebatePerInviteeCap,
|
||||
AdminRechargeRebateEnabled: settings.AdminRechargeRebateEnabled,
|
||||
DefaultUserRPMLimit: settings.DefaultUserRPMLimit,
|
||||
DefaultSubscriptions: defaultSubscriptions,
|
||||
EnableModelFallback: settings.EnableModelFallback,
|
||||
|
||||
@@ -308,6 +308,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if before.AffiliateRebatePerInviteeCap != after.AffiliateRebatePerInviteeCap {
|
||||
changed = append(changed, "affiliate_rebate_per_invitee_cap")
|
||||
}
|
||||
if before.AdminRechargeRebateEnabled != after.AdminRechargeRebateEnabled {
|
||||
changed = append(changed, "affiliate_admin_recharge_enabled")
|
||||
}
|
||||
if !equalDefaultSubscriptions(before.DefaultSubscriptions, after.DefaultSubscriptions) {
|
||||
changed = append(changed, "default_subscriptions")
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ type UpdateSettingsRequest struct {
|
||||
AffiliateRebateFreezeHours *int `json:"affiliate_rebate_freeze_hours"`
|
||||
AffiliateRebateDurationDays *int `json:"affiliate_rebate_duration_days"`
|
||||
AffiliateRebatePerInviteeCap *float64 `json:"affiliate_rebate_per_invitee_cap"`
|
||||
AdminRechargeRebateEnabled *bool `json:"affiliate_admin_recharge_enabled"`
|
||||
DefaultUserRPMLimit int `json:"default_user_rpm_limit"`
|
||||
DefaultSubscriptions []dto.DefaultSubscriptionSetting `json:"default_subscriptions"`
|
||||
AuthSourceDefaultEmailBalance *float64 `json:"auth_source_default_email_balance"`
|
||||
@@ -391,6 +392,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
if affiliateRebatePerInviteeCap < 0 {
|
||||
affiliateRebatePerInviteeCap = service.AffiliateRebatePerInviteeCapDefault
|
||||
}
|
||||
adminRechargeRebateEnabled := previousSettings.AdminRechargeRebateEnabled
|
||||
if req.AdminRechargeRebateEnabled != nil {
|
||||
adminRechargeRebateEnabled = *req.AdminRechargeRebateEnabled
|
||||
}
|
||||
// 通用表格配置:兼容旧客户端未传字段时保留当前值。
|
||||
if req.TableDefaultPageSize <= 0 {
|
||||
req.TableDefaultPageSize = previousSettings.TableDefaultPageSize
|
||||
@@ -1279,6 +1284,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
AffiliateRebateFreezeHours: affiliateRebateFreezeHours,
|
||||
AffiliateRebateDurationDays: affiliateRebateDurationDays,
|
||||
AffiliateRebatePerInviteeCap: affiliateRebatePerInviteeCap,
|
||||
AdminRechargeRebateEnabled: adminRechargeRebateEnabled,
|
||||
DefaultUserRPMLimit: req.DefaultUserRPMLimit,
|
||||
DefaultSubscriptions: defaultSubscriptions,
|
||||
EnableModelFallback: req.EnableModelFallback,
|
||||
@@ -1786,6 +1792,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
AffiliateRebateFreezeHours: updatedSettings.AffiliateRebateFreezeHours,
|
||||
AffiliateRebateDurationDays: updatedSettings.AffiliateRebateDurationDays,
|
||||
AffiliateRebatePerInviteeCap: updatedSettings.AffiliateRebatePerInviteeCap,
|
||||
AdminRechargeRebateEnabled: updatedSettings.AdminRechargeRebateEnabled,
|
||||
DefaultUserRPMLimit: updatedSettings.DefaultUserRPMLimit,
|
||||
DefaultSubscriptions: updatedDefaultSubscriptions,
|
||||
EnableModelFallback: updatedSettings.EnableModelFallback,
|
||||
|
||||
@@ -148,6 +148,7 @@ type SystemSettings struct {
|
||||
AffiliateRebateFreezeHours int `json:"affiliate_rebate_freeze_hours"`
|
||||
AffiliateRebateDurationDays int `json:"affiliate_rebate_duration_days"`
|
||||
AffiliateRebatePerInviteeCap float64 `json:"affiliate_rebate_per_invitee_cap"`
|
||||
AdminRechargeRebateEnabled bool `json:"affiliate_admin_recharge_enabled"`
|
||||
DefaultUserRPMLimit int `json:"default_user_rpm_limit"`
|
||||
DefaultSubscriptions []DefaultSubscriptionSetting `json:"default_subscriptions"`
|
||||
|
||||
|
||||
@@ -836,6 +836,7 @@ func TestAPIContracts(t *testing.T) {
|
||||
"affiliate_rebate_freeze_hours": 0,
|
||||
"affiliate_rebate_duration_days": 0,
|
||||
"affiliate_rebate_per_invitee_cap": 0,
|
||||
"affiliate_admin_recharge_enabled": false,
|
||||
"default_user_rpm_limit": 0,
|
||||
"default_subscriptions": [],
|
||||
"enable_model_fallback": false,
|
||||
@@ -1111,6 +1112,7 @@ func TestAPIContracts(t *testing.T) {
|
||||
"affiliate_rebate_freeze_hours": 0,
|
||||
"affiliate_rebate_duration_days": 0,
|
||||
"affiliate_rebate_per_invitee_cap": 0,
|
||||
"affiliate_admin_recharge_enabled": false,
|
||||
"default_user_rpm_limit": 0,
|
||||
"default_subscriptions": [],
|
||||
"enable_model_fallback": false,
|
||||
@@ -1372,7 +1374,7 @@ func newContractDeps(t *testing.T) *contractDeps {
|
||||
settingRepo := newStubSettingRepo()
|
||||
settingService := service.NewSettingService(settingRepo, cfg)
|
||||
|
||||
adminService := service.NewAdminService(userRepo, groupRepo, &accountRepo, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
adminService := service.NewAdminService(userRepo, groupRepo, &accountRepo, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil, redeemService, nil, nil)
|
||||
apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
|
||||
usageHandler := handler.NewUsageHandler(usageService, apiKeyService, nil, nil)
|
||||
|
||||
@@ -599,6 +599,11 @@ type adminServiceImpl struct {
|
||||
userSubRepo UserSubscriptionRepository
|
||||
privacyClientFactory PrivacyClientFactory
|
||||
runtimeBlocker AccountRuntimeBlocker
|
||||
affiliateService adminRechargeAffiliateAccruer
|
||||
}
|
||||
|
||||
type adminRechargeAffiliateAccruer interface {
|
||||
AccrueInviteRebate(ctx context.Context, inviteeUserID int64, baseRechargeAmount float64) (float64, error)
|
||||
}
|
||||
|
||||
type userGroupRateBatchReader interface {
|
||||
@@ -625,6 +630,7 @@ func NewAdminService(
|
||||
userSubRepo UserSubscriptionRepository,
|
||||
privacyClientFactory PrivacyClientFactory,
|
||||
runtimeBlocker AccountRuntimeBlocker,
|
||||
affiliateService *AffiliateService,
|
||||
) AdminService {
|
||||
return &adminServiceImpl{
|
||||
userRepo: userRepo,
|
||||
@@ -646,5 +652,6 @@ func NewAdminService(
|
||||
userSubRepo: userSubRepo,
|
||||
privacyClientFactory: privacyClientFactory,
|
||||
runtimeBlocker: runtimeBlocker,
|
||||
affiliateService: affiliateService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -50,6 +51,30 @@ type authCacheInvalidatorStub struct {
|
||||
keys []string
|
||||
}
|
||||
|
||||
type adminRechargeAffiliateAccruerStub struct {
|
||||
calls []adminRechargeAffiliateAccrual
|
||||
rebate float64
|
||||
err error
|
||||
}
|
||||
|
||||
type adminRechargeAffiliateAccrual struct {
|
||||
userID int64
|
||||
amount float64
|
||||
}
|
||||
|
||||
func (s *adminRechargeAffiliateAccruerStub) AccrueInviteRebate(_ context.Context, userID int64, amount float64) (float64, error) {
|
||||
s.calls = append(s.calls, adminRechargeAffiliateAccrual{userID: userID, amount: amount})
|
||||
return s.rebate, s.err
|
||||
}
|
||||
|
||||
func adminRechargeSettingService(enabled bool) *SettingService {
|
||||
values := map[string]string{}
|
||||
if enabled {
|
||||
values[SettingKeyAffiliateAdminRechargeEnabled] = "true"
|
||||
}
|
||||
return NewSettingService(&settingRepoStub{values: values}, nil)
|
||||
}
|
||||
|
||||
func (s *authCacheInvalidatorStub) InvalidateAuthCacheByKey(ctx context.Context, key string) {
|
||||
s.keys = append(s.keys, key)
|
||||
}
|
||||
@@ -95,3 +120,76 @@ func TestAdminService_UpdateUserBalance_NoChangeNoInvalidate(t *testing.T) {
|
||||
require.Empty(t, invalidator.userIDs)
|
||||
require.Empty(t, redeemRepo.created)
|
||||
}
|
||||
|
||||
func TestAdminService_UpdateUserBalance_AdminRechargeAffiliateRebate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enabled bool
|
||||
operation string
|
||||
amount float64
|
||||
wantCalls []adminRechargeAffiliateAccrual
|
||||
}{
|
||||
{
|
||||
name: "disabled by default",
|
||||
operation: "add",
|
||||
amount: 5,
|
||||
},
|
||||
{
|
||||
name: "enabled add",
|
||||
enabled: true,
|
||||
operation: "add",
|
||||
amount: 0.1,
|
||||
wantCalls: []adminRechargeAffiliateAccrual{{userID: 7, amount: 0.1}},
|
||||
},
|
||||
{
|
||||
name: "enabled set increase",
|
||||
enabled: true,
|
||||
operation: "set",
|
||||
amount: 15,
|
||||
},
|
||||
{
|
||||
name: "enabled subtract",
|
||||
enabled: true,
|
||||
operation: "subtract",
|
||||
amount: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
baseRepo := &userRepoStub{user: &User{ID: 7, Balance: 10}}
|
||||
repo := &balanceUserRepoStub{userRepoStub: baseRepo}
|
||||
redeemRepo := &balanceRedeemRepoStub{redeemRepoStub: &redeemRepoStub{}}
|
||||
affiliate := &adminRechargeAffiliateAccruerStub{}
|
||||
svc := &adminServiceImpl{
|
||||
userRepo: repo,
|
||||
redeemCodeRepo: redeemRepo,
|
||||
settingService: adminRechargeSettingService(tt.enabled),
|
||||
affiliateService: affiliate,
|
||||
}
|
||||
|
||||
_, err := svc.UpdateUserBalance(context.Background(), 7, tt.amount, tt.operation, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantCalls, affiliate.calls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminService_UpdateUserBalance_AffiliateFailureDoesNotRollbackRecharge(t *testing.T) {
|
||||
baseRepo := &userRepoStub{user: &User{ID: 7, Balance: 10}}
|
||||
repo := &balanceUserRepoStub{userRepoStub: baseRepo}
|
||||
redeemRepo := &balanceRedeemRepoStub{redeemRepoStub: &redeemRepoStub{}}
|
||||
affiliate := &adminRechargeAffiliateAccruerStub{err: errors.New("affiliate unavailable")}
|
||||
svc := &adminServiceImpl{
|
||||
userRepo: repo,
|
||||
redeemCodeRepo: redeemRepo,
|
||||
settingService: adminRechargeSettingService(true),
|
||||
affiliateService: affiliate,
|
||||
}
|
||||
|
||||
user, err := svc.UpdateUserBalance(context.Background(), 7, 5, "add", "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 15.0, user.Balance)
|
||||
require.Equal(t, []adminRechargeAffiliateAccrual{{userID: 7, amount: 5}}, affiliate.calls)
|
||||
require.Len(t, redeemRepo.created, 1)
|
||||
}
|
||||
|
||||
@@ -487,6 +487,7 @@ func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64,
|
||||
if s.authCacheInvalidator != nil && balanceDiff != 0 {
|
||||
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
|
||||
}
|
||||
s.tryAccrueAffiliateRebateForAdminRecharge(ctx, userID, operation, balance)
|
||||
|
||||
if s.billingCacheService != nil {
|
||||
go func() {
|
||||
@@ -524,6 +525,24 @@ func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64,
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) tryAccrueAffiliateRebateForAdminRecharge(ctx context.Context, userID int64, operation string, amount float64) {
|
||||
if operation != "add" || amount <= 0 || s.settingService == nil || s.affiliateService == nil {
|
||||
return
|
||||
}
|
||||
if !s.settingService.IsAffiliateAdminRechargeEnabled(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
rebate, err := s.affiliateService.AccrueInviteRebate(ctx, userID, amount)
|
||||
if err != nil {
|
||||
logger.LegacyPrintf("service.admin", "affiliate rebate failed for admin recharge: user_id=%d amount=%.8f err=%v", userID, amount, err)
|
||||
return
|
||||
}
|
||||
if rebate > 0 {
|
||||
logger.LegacyPrintf("service.admin", "affiliate rebate accrued for admin recharge: user_id=%d amount=%.8f rebate=%.8f", userID, amount, rebate)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int, sortBy, sortOrder string) ([]APIKey, int64, error) {
|
||||
params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
|
||||
keys, result, err := s.apiKeyRepo.ListByUserID(ctx, userID, params, APIKeyListFilters{})
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
AffiliateRebateDurationDaysDefault = 0 // 0 = 永久有效
|
||||
AffiliateRebateDurationDaysMax = 3650 // ~10 年
|
||||
AffiliateRebatePerInviteeCapDefault = 0.0 // 0 = 无上限
|
||||
AdminRechargeRebateEnabledDefault = false // 管理员充值默认不产生返利
|
||||
)
|
||||
|
||||
// Platform constants
|
||||
@@ -138,6 +139,7 @@ const (
|
||||
SettingKeyAffiliateRebateFreezeHours = "affiliate_rebate_freeze_hours" // 返利冻结期(小时,0=不冻结)
|
||||
SettingKeyAffiliateRebateDurationDays = "affiliate_rebate_duration_days" // 返利有效期(天,0=永久)
|
||||
SettingKeyAffiliateRebatePerInviteeCap = "affiliate_rebate_per_invitee_cap" // 单人返利上限(0=无上限)
|
||||
SettingKeyAffiliateAdminRechargeEnabled = "affiliate_admin_recharge_enabled" // 管理员充值是否产生返利
|
||||
SettingKeyRiskControlEnabled = "risk_control_enabled" // 是否启用风控中心入口与审计链路
|
||||
SettingKeyContentModerationConfig = "content_moderation_config" // 内容审计配置(JSON)
|
||||
SettingKeyCyberSessionBlockEnabled = "cyber_session_block_enabled" // cyber 命中后会话级自动屏蔽总开关(默认关)
|
||||
|
||||
@@ -77,6 +77,16 @@ func (s *SettingService) IsAffiliateEnabled(ctx context.Context) bool {
|
||||
return value == "true"
|
||||
}
|
||||
|
||||
// IsAffiliateAdminRechargeEnabled reports whether admin balance
|
||||
// deposits should participate in the affiliate rebate program.
|
||||
func (s *SettingService) IsAffiliateAdminRechargeEnabled(ctx context.Context) bool {
|
||||
value, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateAdminRechargeEnabled)
|
||||
if err != nil {
|
||||
return AdminRechargeRebateEnabledDefault
|
||||
}
|
||||
return value == "true"
|
||||
}
|
||||
|
||||
// GetAffiliateRebateRatePercent 读取并 clamp 全局返利比例。
|
||||
// 解析失败、缺失或越界都回退到 AffiliateRebateRateDefault — 该比例从不抛错,
|
||||
// 调用方只关心一个可用的数值。
|
||||
|
||||
@@ -181,7 +181,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
||||
SettingKeyAvailableChannelsEnabled: "false",
|
||||
|
||||
// Affiliate (邀请返利) feature (default disabled; opt-in)
|
||||
SettingKeyAffiliateEnabled: "false",
|
||||
SettingKeyAffiliateEnabled: "false",
|
||||
SettingKeyAffiliateAdminRechargeEnabled: strconv.FormatBool(AdminRechargeRebateEnabledDefault),
|
||||
|
||||
// 风控中心功能(默认关闭,显式启用)
|
||||
SettingKeyRiskControlEnabled: "false",
|
||||
@@ -332,6 +333,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
if perInviteeCap, err := strconv.ParseFloat(settings[SettingKeyAffiliateRebatePerInviteeCap], 64); err == nil && perInviteeCap >= 0 {
|
||||
result.AffiliateRebatePerInviteeCap = perInviteeCap
|
||||
}
|
||||
result.AdminRechargeRebateEnabled = settings[SettingKeyAffiliateAdminRechargeEnabled] == "true"
|
||||
result.DefaultSubscriptions = parseDefaultSubscriptions(settings[SettingKeyDefaultSubscriptions])
|
||||
|
||||
// 敏感信息直接返回,方便测试连接时使用
|
||||
|
||||
@@ -126,6 +126,37 @@ type defaultSubGroupReaderStub struct {
|
||||
calls []int64
|
||||
}
|
||||
|
||||
func TestSettingService_AffiliateAdminRechargeSetting(t *testing.T) {
|
||||
t.Run("missing value defaults to disabled", func(t *testing.T) {
|
||||
svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{}}, &config.Config{})
|
||||
|
||||
settings, err := svc.GetAllSettings(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.False(t, settings.AdminRechargeRebateEnabled)
|
||||
})
|
||||
|
||||
t.Run("explicit value is parsed", func(t *testing.T) {
|
||||
svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{
|
||||
SettingKeyAffiliateAdminRechargeEnabled: "true",
|
||||
}}, &config.Config{})
|
||||
|
||||
settings, err := svc.GetAllSettings(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.True(t, settings.AdminRechargeRebateEnabled)
|
||||
})
|
||||
|
||||
t.Run("value is persisted", func(t *testing.T) {
|
||||
repo := &settingUpdateRepoStub{}
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
err := svc.UpdateSettings(context.Background(), &SystemSettings{
|
||||
AdminRechargeRebateEnabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "true", repo.updates[SettingKeyAffiliateAdminRechargeEnabled])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *defaultSubGroupReaderStub) GetByID(ctx context.Context, id int64) (*Group, error) {
|
||||
s.calls = append(s.calls, id)
|
||||
if err, ok := s.errBy[id]; ok {
|
||||
|
||||
@@ -297,6 +297,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
||||
settings.AffiliateRebatePerInviteeCap = AffiliateRebatePerInviteeCapDefault
|
||||
}
|
||||
updates[SettingKeyAffiliateRebatePerInviteeCap] = strconv.FormatFloat(settings.AffiliateRebatePerInviteeCap, 'f', 8, 64)
|
||||
updates[SettingKeyAffiliateAdminRechargeEnabled] = strconv.FormatBool(settings.AdminRechargeRebateEnabled)
|
||||
updates[SettingKeyDefaultUserRPMLimit] = strconv.Itoa(settings.DefaultUserRPMLimit)
|
||||
defaultSubsJSON, err := json.Marshal(settings.DefaultSubscriptions)
|
||||
if err != nil {
|
||||
|
||||
@@ -152,6 +152,7 @@ type SystemSettings struct {
|
||||
AffiliateRebateFreezeHours int
|
||||
AffiliateRebateDurationDays int
|
||||
AffiliateRebatePerInviteeCap float64
|
||||
AdminRechargeRebateEnabled bool
|
||||
DefaultUserRPMLimit int
|
||||
DefaultSubscriptions []DefaultSubscriptionSetting
|
||||
|
||||
|
||||
@@ -376,6 +376,7 @@ export interface SystemSettings {
|
||||
affiliate_rebate_freeze_hours: number;
|
||||
affiliate_rebate_duration_days: number;
|
||||
affiliate_rebate_per_invitee_cap: number;
|
||||
affiliate_admin_recharge_enabled: boolean;
|
||||
default_concurrency: number;
|
||||
default_user_rpm_limit: number;
|
||||
default_subscriptions: DefaultSubscriptionSetting[];
|
||||
@@ -673,6 +674,7 @@ export interface UpdateSettingsRequest {
|
||||
affiliate_rebate_freeze_hours?: number;
|
||||
affiliate_rebate_duration_days?: number;
|
||||
affiliate_rebate_per_invitee_cap?: number;
|
||||
affiliate_admin_recharge_enabled?: boolean;
|
||||
default_concurrency?: number;
|
||||
default_user_rpm_limit?: number;
|
||||
default_subscriptions?: DefaultSubscriptionSetting[];
|
||||
|
||||
@@ -45,6 +45,8 @@ export default {
|
||||
description: 'Existing users invite new ones; the inviter earns a percentage rebate on the invitee’s recharges. Disabled by default.',
|
||||
enabled: 'Enable Affiliate',
|
||||
enabledHint: 'When off, the affiliate menu is hidden, the aff parameter is ignored at signup, and new recharges generate no rebate. Existing rebate balances can still be transferred.',
|
||||
adminRechargeRebate: 'Rebate Admin Deposits',
|
||||
adminRechargeRebateHint: 'When enabled, balance added through User Management > Deposit generates affiliate rebates. Setting a balance or withdrawing funds does not.',
|
||||
rebateRate: 'Global Rebate Rate',
|
||||
rebateRateHint: 'Default percentage given back to the inviter on recharges (0-100, e.g. 10 = 10%).',
|
||||
freezeHours: 'Rebate Freeze Period (hours)',
|
||||
|
||||
@@ -45,6 +45,8 @@ export default {
|
||||
description: '老用户邀请新用户注册,新用户充值后老用户按比例获得返利额度。默认关闭。',
|
||||
enabled: '启用邀请返利',
|
||||
enabledHint: '关闭后用户菜单中的邀请页面入口隐藏、注册时忽略邀请码、新充值不再产生返利。已有返利额度仍可转入余额。',
|
||||
adminRechargeRebate: '管理员充值参与返利',
|
||||
adminRechargeRebateHint: '开启后,通过“用户管理 > 充值”增加的余额会产生邀请返利;设置余额和扣款不参与返利。',
|
||||
rebateRate: '全局返利比例',
|
||||
rebateRateHint: '充值后返给邀请人的默认比例(0-100%,例如填写 10 表示返利 10%)。',
|
||||
freezeHours: '返利冻结期(小时)',
|
||||
|
||||
@@ -5938,6 +5938,18 @@
|
||||
</div>
|
||||
|
||||
<div v-if="form.affiliate_enabled" class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.features.affiliate.adminRechargeRebate') }}
|
||||
</label>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.features.affiliate.adminRechargeRebateHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.affiliate_admin_recharge_enabled" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">
|
||||
{{ t('admin.settings.features.affiliate.rebateRate') }}
|
||||
@@ -8063,6 +8075,7 @@ const form = reactive<SettingsForm>({
|
||||
affiliate_rebate_freeze_hours: 0,
|
||||
affiliate_rebate_duration_days: 0,
|
||||
affiliate_rebate_per_invitee_cap: 0,
|
||||
affiliate_admin_recharge_enabled: false,
|
||||
default_concurrency: 1,
|
||||
default_subscriptions: [],
|
||||
force_email_on_third_party_signup: false,
|
||||
@@ -9418,6 +9431,7 @@ async function saveSettings() {
|
||||
affiliate_rebate_freeze_hours: Math.max(0, Math.min(720, Number(form.affiliate_rebate_freeze_hours) || 0)),
|
||||
affiliate_rebate_duration_days: Math.max(0, Math.min(3650, Math.floor(Number(form.affiliate_rebate_duration_days) || 0))),
|
||||
affiliate_rebate_per_invitee_cap: Math.max(0, Number(form.affiliate_rebate_per_invitee_cap) || 0),
|
||||
affiliate_admin_recharge_enabled: form.affiliate_admin_recharge_enabled,
|
||||
default_concurrency: form.default_concurrency,
|
||||
default_subscriptions: normalizedDefaultSubscriptions,
|
||||
force_email_on_third_party_signup: form.force_email_on_third_party_signup,
|
||||
|
||||
@@ -646,6 +646,27 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
expect(payload).not.toHaveProperty("payment_visible_method_wxpay_enabled");
|
||||
});
|
||||
|
||||
it("submits the admin recharge affiliate rebate setting", async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...baseSettingsResponse,
|
||||
affiliate_enabled: true,
|
||||
affiliate_admin_recharge_enabled: true,
|
||||
});
|
||||
|
||||
const wrapper = mountView();
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledTimes(1);
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
affiliate_admin_recharge_enabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("submits Anthropic cache TTL injection gateway setting", async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...baseSettingsResponse,
|
||||
|
||||
Reference in New Issue
Block a user