diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 86721fbac0..47c7ced911 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -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) diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 1d506f505b..8355b1f16d 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -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, diff --git a/backend/internal/handler/admin/setting_handler_audit.go b/backend/internal/handler/admin/setting_handler_audit.go index 899030d1b9..e4fe62625e 100644 --- a/backend/internal/handler/admin/setting_handler_audit.go +++ b/backend/internal/handler/admin/setting_handler_audit.go @@ -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") } diff --git a/backend/internal/handler/admin/setting_handler_update.go b/backend/internal/handler/admin/setting_handler_update.go index b4c1f3afc1..157bb897c2 100644 --- a/backend/internal/handler/admin/setting_handler_update.go +++ b/backend/internal/handler/admin/setting_handler_update.go @@ -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, diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 5b7e657f26..4678c73048 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -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"` diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index da766a753b..f1fd4230be 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -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) diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 964566a80c..a09bd14fb7 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -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, } } diff --git a/backend/internal/service/admin_service_update_balance_test.go b/backend/internal/service/admin_service_update_balance_test.go index d3b3c70072..48b89f8ceb 100644 --- a/backend/internal/service/admin_service_update_balance_test.go +++ b/backend/internal/service/admin_service_update_balance_test.go @@ -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) +} diff --git a/backend/internal/service/admin_user.go b/backend/internal/service/admin_user.go index 862a5ab5c7..87fe2346a1 100644 --- a/backend/internal/service/admin_user.go +++ b/backend/internal/service/admin_user.go @@ -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{}) diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index bc1db19105..44631e3f3c 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -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 命中后会话级自动屏蔽总开关(默认关) diff --git a/backend/internal/service/setting_features.go b/backend/internal/service/setting_features.go index 57c036861e..11c8af9dbc 100644 --- a/backend/internal/service/setting_features.go +++ b/backend/internal/service/setting_features.go @@ -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 — 该比例从不抛错, // 调用方只关心一个可用的数值。 diff --git a/backend/internal/service/setting_parse.go b/backend/internal/service/setting_parse.go index bf71b559a0..1ffbf4c873 100644 --- a/backend/internal/service/setting_parse.go +++ b/backend/internal/service/setting_parse.go @@ -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]) // 敏感信息直接返回,方便测试连接时使用 diff --git a/backend/internal/service/setting_service_update_test.go b/backend/internal/service/setting_service_update_test.go index a47e1379f7..60caf4421d 100644 --- a/backend/internal/service/setting_service_update_test.go +++ b/backend/internal/service/setting_service_update_test.go @@ -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 { diff --git a/backend/internal/service/setting_update.go b/backend/internal/service/setting_update.go index ba819a5ce6..d50a7b125e 100644 --- a/backend/internal/service/setting_update.go +++ b/backend/internal/service/setting_update.go @@ -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 { diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 285df80ba3..7e15a1a468 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -152,6 +152,7 @@ type SystemSettings struct { AffiliateRebateFreezeHours int AffiliateRebateDurationDays int AffiliateRebatePerInviteeCap float64 + AdminRechargeRebateEnabled bool DefaultUserRPMLimit int DefaultSubscriptions []DefaultSubscriptionSetting diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 55c8088be1..4a3933fe02 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -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[]; diff --git a/frontend/src/i18n/locales/en/admin/settings.ts b/frontend/src/i18n/locales/en/admin/settings.ts index e08be9f591..c8cc6c23f9 100644 --- a/frontend/src/i18n/locales/en/admin/settings.ts +++ b/frontend/src/i18n/locales/en/admin/settings.ts @@ -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)', diff --git a/frontend/src/i18n/locales/zh/admin/settings.ts b/frontend/src/i18n/locales/zh/admin/settings.ts index 08c0dbcd53..3a294c1617 100644 --- a/frontend/src/i18n/locales/zh/admin/settings.ts +++ b/frontend/src/i18n/locales/zh/admin/settings.ts @@ -45,6 +45,8 @@ export default { description: '老用户邀请新用户注册,新用户充值后老用户按比例获得返利额度。默认关闭。', enabled: '启用邀请返利', enabledHint: '关闭后用户菜单中的邀请页面入口隐藏、注册时忽略邀请码、新充值不再产生返利。已有返利额度仍可转入余额。', + adminRechargeRebate: '管理员充值参与返利', + adminRechargeRebateHint: '开启后,通过“用户管理 > 充值”增加的余额会产生邀请返利;设置余额和扣款不参与返利。', rebateRate: '全局返利比例', rebateRateHint: '充值后返给邀请人的默认比例(0-100%,例如填写 10 表示返利 10%)。', freezeHours: '返利冻结期(小时)', diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 26253c7eb8..8b1df9d3a6 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -5938,6 +5938,18 @@
+ {{ t('admin.settings.features.affiliate.adminRechargeRebateHint') }} +
+