feat: add affiliate invite rebate flow and admin rebate-rate setting

This commit is contained in:
VpSanta33
2026-04-24 21:41:26 +08:00
parent d162604f32
commit f03de00cb9
33 changed files with 1744 additions and 42 deletions

View File

@@ -0,0 +1,288 @@
package service
import (
"context"
"errors"
"math"
"strconv"
"strings"
"time"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
var (
ErrAffiliateProfileNotFound = infraerrors.NotFound("AFFILIATE_PROFILE_NOT_FOUND", "affiliate profile not found")
ErrAffiliateCodeInvalid = infraerrors.BadRequest("AFFILIATE_CODE_INVALID", "invalid affiliate code")
ErrAffiliateAlreadyBound = infraerrors.Conflict("AFFILIATE_ALREADY_BOUND", "affiliate inviter already bound")
ErrAffiliateQuotaEmpty = infraerrors.BadRequest("AFFILIATE_QUOTA_EMPTY", "no affiliate quota available to transfer")
)
const (
affiliateInviteesLimit = 100
)
type AffiliateSummary struct {
UserID int64 `json:"user_id"`
AffCode string `json:"aff_code"`
InviterID *int64 `json:"inviter_id,omitempty"`
AffCount int `json:"aff_count"`
AffQuota float64 `json:"aff_quota"`
AffHistoryQuota float64 `json:"aff_history_quota"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type AffiliateInvitee struct {
UserID int64 `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
CreatedAt *time.Time `json:"created_at,omitempty"`
}
type AffiliateDetail struct {
UserID int64 `json:"user_id"`
AffCode string `json:"aff_code"`
InviterID *int64 `json:"inviter_id,omitempty"`
AffCount int `json:"aff_count"`
AffQuota float64 `json:"aff_quota"`
AffHistoryQuota float64 `json:"aff_history_quota"`
Invitees []AffiliateInvitee `json:"invitees"`
}
type AffiliateRepository interface {
EnsureUserAffiliate(ctx context.Context, userID int64) (*AffiliateSummary, error)
GetAffiliateByCode(ctx context.Context, code string) (*AffiliateSummary, error)
BindInviter(ctx context.Context, userID, inviterID int64) (bool, error)
AccrueQuota(ctx context.Context, inviterID, inviteeUserID int64, amount float64) (bool, error)
TransferQuotaToBalance(ctx context.Context, userID int64) (float64, float64, error)
ListInvitees(ctx context.Context, inviterID int64, limit int) ([]AffiliateInvitee, error)
}
type AffiliateService struct {
repo AffiliateRepository
settingRepo SettingRepository
authCacheInvalidator APIKeyAuthCacheInvalidator
billingCacheService *BillingCacheService
}
func NewAffiliateService(repo AffiliateRepository, settingRepo SettingRepository, authCacheInvalidator APIKeyAuthCacheInvalidator, billingCacheService *BillingCacheService) *AffiliateService {
return &AffiliateService{
repo: repo,
settingRepo: settingRepo,
authCacheInvalidator: authCacheInvalidator,
billingCacheService: billingCacheService,
}
}
func (s *AffiliateService) EnsureUserAffiliate(ctx context.Context, userID int64) (*AffiliateSummary, error) {
if userID <= 0 {
return nil, infraerrors.BadRequest("INVALID_USER", "invalid user")
}
if s == nil || s.repo == nil {
return nil, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "affiliate service unavailable")
}
return s.repo.EnsureUserAffiliate(ctx, userID)
}
func (s *AffiliateService) GetAffiliateDetail(ctx context.Context, userID int64) (*AffiliateDetail, error) {
summary, err := s.EnsureUserAffiliate(ctx, userID)
if err != nil {
return nil, err
}
invitees, err := s.listInvitees(ctx, userID)
if err != nil {
return nil, err
}
return &AffiliateDetail{
UserID: summary.UserID,
AffCode: summary.AffCode,
InviterID: summary.InviterID,
AffCount: summary.AffCount,
AffQuota: summary.AffQuota,
AffHistoryQuota: summary.AffHistoryQuota,
Invitees: invitees,
}, nil
}
func (s *AffiliateService) BindInviterByCode(ctx context.Context, userID int64, rawCode string) error {
code := strings.ToUpper(strings.TrimSpace(rawCode))
if code == "" {
return nil
}
if s == nil || s.repo == nil {
return infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "affiliate service unavailable")
}
selfSummary, err := s.repo.EnsureUserAffiliate(ctx, userID)
if err != nil {
return err
}
if selfSummary.InviterID != nil {
return nil
}
inviterSummary, err := s.repo.GetAffiliateByCode(ctx, code)
if err != nil {
if errors.Is(err, ErrAffiliateProfileNotFound) {
return ErrAffiliateCodeInvalid
}
return err
}
if inviterSummary == nil || inviterSummary.UserID <= 0 || inviterSummary.UserID == userID {
return ErrAffiliateCodeInvalid
}
bound, err := s.repo.BindInviter(ctx, userID, inviterSummary.UserID)
if err != nil {
return err
}
if !bound {
return ErrAffiliateAlreadyBound
}
return nil
}
func (s *AffiliateService) AccrueInviteRebate(ctx context.Context, inviteeUserID int64, baseRechargeAmount float64) (float64, error) {
if s == nil || s.repo == nil {
return 0, nil
}
if inviteeUserID <= 0 || baseRechargeAmount <= 0 || math.IsNaN(baseRechargeAmount) || math.IsInf(baseRechargeAmount, 0) {
return 0, nil
}
inviteeSummary, err := s.repo.EnsureUserAffiliate(ctx, inviteeUserID)
if err != nil {
return 0, err
}
if inviteeSummary.InviterID == nil || *inviteeSummary.InviterID <= 0 {
return 0, nil
}
rebateRatePercent := s.loadAffiliateRebateRatePercent(ctx)
rebate := roundTo(baseRechargeAmount*(rebateRatePercent/100), 8)
if rebate <= 0 {
return 0, nil
}
if _, err := s.repo.EnsureUserAffiliate(ctx, *inviteeSummary.InviterID); err != nil {
return 0, err
}
applied, err := s.repo.AccrueQuota(ctx, *inviteeSummary.InviterID, inviteeUserID, rebate)
if err != nil {
return 0, err
}
if !applied {
return 0, nil
}
return rebate, nil
}
func (s *AffiliateService) TransferAffiliateQuota(ctx context.Context, userID int64) (float64, float64, error) {
if s == nil || s.repo == nil {
return 0, 0, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "affiliate service unavailable")
}
transferred, balance, err := s.repo.TransferQuotaToBalance(ctx, userID)
if err != nil {
return 0, 0, err
}
if transferred > 0 {
s.invalidateAffiliateCaches(ctx, userID)
}
return transferred, balance, nil
}
func (s *AffiliateService) listInvitees(ctx context.Context, inviterID int64) ([]AffiliateInvitee, error) {
if s == nil || s.repo == nil {
return nil, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "affiliate service unavailable")
}
invitees, err := s.repo.ListInvitees(ctx, inviterID, affiliateInviteesLimit)
if err != nil {
return nil, err
}
for i := range invitees {
invitees[i].Email = maskEmail(invitees[i].Email)
}
return invitees, nil
}
func (s *AffiliateService) loadAffiliateRebateRatePercent(ctx context.Context) float64 {
if s == nil || s.settingRepo == nil {
return AffiliateRebateRateDefault
}
raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebateRate)
if err != nil {
return AffiliateRebateRateDefault
}
rate, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
if err != nil {
return AffiliateRebateRateDefault
}
if math.IsNaN(rate) || math.IsInf(rate, 0) {
return AffiliateRebateRateDefault
}
if rate < AffiliateRebateRateMin {
return AffiliateRebateRateMin
}
if rate > AffiliateRebateRateMax {
return AffiliateRebateRateMax
}
return rate
}
func roundTo(v float64, scale int) float64 {
factor := math.Pow10(scale)
return math.Round(v*factor) / factor
}
func maskEmail(email string) string {
email = strings.TrimSpace(email)
if email == "" {
return ""
}
at := strings.Index(email, "@")
if at <= 0 || at >= len(email)-1 {
return "***"
}
local := email[:at]
domain := email[at+1:]
dot := strings.LastIndex(domain, ".")
maskedLocal := maskSegment(local)
if dot <= 0 || dot >= len(domain)-1 {
return maskedLocal + "@" + maskSegment(domain)
}
domainName := domain[:dot]
tld := domain[dot:]
return maskedLocal + "@" + maskSegment(domainName) + tld
}
func maskSegment(s string) string {
r := []rune(s)
if len(r) == 0 {
return "***"
}
if len(r) == 1 {
return string(r[0]) + "***"
}
return string(r[0]) + "***"
}
func (s *AffiliateService) invalidateAffiliateCaches(ctx context.Context, userID int64) {
if s.authCacheInvalidator != nil {
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
}
if s.billingCacheService != nil {
go func() {
cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = s.billingCacheService.InvalidateUserBalance(cacheCtx, userID)
}()
}
}

View File

@@ -0,0 +1,59 @@
//go:build unit
package service
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
type affiliateSettingRepoStub struct {
value string
err error
}
func (s *affiliateSettingRepoStub) Get(context.Context, string) (*Setting, error) { return nil, s.err }
func (s *affiliateSettingRepoStub) GetValue(context.Context, string) (string, error) {
if s.err != nil {
return "", s.err
}
return s.value, nil
}
func (s *affiliateSettingRepoStub) Set(context.Context, string, string) error { return s.err }
func (s *affiliateSettingRepoStub) GetMultiple(context.Context, []string) (map[string]string, error) {
if s.err != nil {
return nil, s.err
}
return map[string]string{}, nil
}
func (s *affiliateSettingRepoStub) SetMultiple(context.Context, map[string]string) error {
return s.err
}
func (s *affiliateSettingRepoStub) GetAll(context.Context) (map[string]string, error) {
if s.err != nil {
return nil, s.err
}
return map[string]string{}, nil
}
func (s *affiliateSettingRepoStub) Delete(context.Context, string) error { return s.err }
func TestAffiliateRebateRatePercentSemantics(t *testing.T) {
t.Parallel()
svc := &AffiliateService{settingRepo: &affiliateSettingRepoStub{value: "1"}}
rate := svc.loadAffiliateRebateRatePercent(context.Background())
require.Equal(t, 1.0, rate)
svc.settingRepo = &affiliateSettingRepoStub{value: "0.2"}
rate = svc.loadAffiliateRebateRatePercent(context.Background())
require.Equal(t, 0.2, rate)
}
func TestMaskEmail(t *testing.T) {
t.Parallel()
require.Equal(t, "a***@g***.com", maskEmail("alice@gmail.com"))
require.Equal(t, "x***@d***", maskEmail("x@domain"))
require.Equal(t, "", maskEmail(""))
}

View File

@@ -72,6 +72,7 @@ type AuthService struct {
turnstileService *TurnstileService
emailQueueService *EmailQueueService
promoService *PromoService
affiliateService *AffiliateService
defaultSubAssigner DefaultSubscriptionAssigner
}
@@ -121,13 +122,26 @@ func (s *AuthService) EntClient() *dbent.Client {
return s.entClient
}
func (s *AuthService) SetAffiliateService(affiliateService *AffiliateService) {
if s == nil {
return
}
s.affiliateService = affiliateService
}
// Register 用户注册返回token和用户
func (s *AuthService) Register(ctx context.Context, email, password string) (string, *User, error) {
return s.RegisterWithVerification(ctx, email, password, "", "", "")
}
// RegisterWithVerification 用户注册支持邮件验证、优惠码和邀请码返回token和用户
func (s *AuthService) RegisterWithVerification(ctx context.Context, email, password, verifyCode, promoCode, invitationCode string) (string, *User, error) {
// RegisterWithVerification 用户注册(支持邮件验证、优惠码、邀请码和邀请返利返回token和用户
// affiliateCode 使用可选参数以兼容旧调用方。
func (s *AuthService) RegisterWithVerification(ctx context.Context, email, password, verifyCode, promoCode, invitationCode string, affiliateCode ...string) (string, *User, error) {
affiliateCodeRaw := ""
if len(affiliateCode) > 0 {
affiliateCodeRaw = affiliateCode[0]
}
// 检查是否开放注册默认关闭settingService 未配置时不允许注册)
if s.settingService == nil || !s.settingService.IsRegistrationEnabled(ctx) {
return "", nil, ErrRegDisabled
@@ -223,6 +237,17 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
}
s.postAuthUserBootstrap(ctx, user, "email", true)
s.assignSubscriptions(ctx, user.ID, grantPlan.Subscriptions, "auto assigned by signup defaults")
if s.affiliateService != nil {
if _, err := s.affiliateService.EnsureUserAffiliate(ctx, user.ID); err != nil {
logger.LegacyPrintf("service.auth", "[Auth] Failed to initialize affiliate profile for user %d: %v", user.ID, err)
}
if code := strings.TrimSpace(affiliateCodeRaw); code != "" {
if err := s.affiliateService.BindInviterByCode(ctx, user.ID, code); err != nil {
// 邀请返利码绑定失败不影响注册,只记录日志
logger.LegacyPrintf("service.auth", "[Auth] Failed to bind affiliate inviter for user %d: %v", user.ID, err)
}
}
}
// 标记邀请码为已使用(如果使用了邀请码)
if invitationRedeemCode != nil {

View File

@@ -18,6 +18,13 @@ const (
RoleUser = domain.RoleUser
)
// Affiliate rebate settings
const (
AffiliateRebateRateDefault = 20.0
AffiliateRebateRateMin = 0.0
AffiliateRebateRateMax = 100.0
)
// Platform constants
const (
PlatformAnthropic = domain.PlatformAnthropic
@@ -87,6 +94,7 @@ const (
SettingKeyPasswordResetEnabled = "password_reset_enabled" // 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyFrontendURL = "frontend_url" // 前端基础URL用于生成邮件中的重置密码链接
SettingKeyInvitationCodeEnabled = "invitation_code_enabled" // 是否启用邀请码注册
SettingKeyAffiliateRebateRate = "affiliate_rebate_rate" // 邀请返利比例百分比0-100
// 邮件服务设置
SettingKeySMTPHost = "smtp_host" // SMTP服务器地址

View File

@@ -2,6 +2,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
@@ -268,6 +269,7 @@ func (s *PaymentService) doBalance(ctx context.Context, o *dbent.PaymentOrder) e
switch action {
case redeemActionSkipCompleted:
s.applyAffiliateRebateForOrder(ctx, o)
// Code already created and redeemed — just mark completed
return s.markCompleted(ctx, o, "RECHARGE_SUCCESS")
case redeemActionCreate:
@@ -281,6 +283,7 @@ func (s *PaymentService) doBalance(ctx context.Context, o *dbent.PaymentOrder) e
if _, err := s.redeemService.Redeem(ctx, o.UserID, o.RechargeCode); err != nil {
return fmt.Errorf("redeem balance: %w", err)
}
s.applyAffiliateRebateForOrder(ctx, o)
return s.markCompleted(ctx, o, "RECHARGE_SUCCESS")
}
@@ -358,6 +361,139 @@ func (s *PaymentService) hasAuditLog(ctx context.Context, orderID int64, action
return c > 0
}
func (s *PaymentService) applyAffiliateRebateForOrder(ctx context.Context, o *dbent.PaymentOrder) {
if o == nil || o.OrderType != payment.OrderTypeBalance || o.Amount <= 0 {
return
}
if s.affiliateService == nil {
return
}
tx, err := s.entClient.Tx(ctx)
if err != nil {
s.writeAuditLog(ctx, o.ID, "AFFILIATE_REBATE_FAILED", "system", map[string]any{
"error": fmt.Sprintf("begin affiliate rebate tx: %v", err),
})
return
}
defer func() { _ = tx.Rollback() }()
txCtx := dbent.NewTxContext(ctx, tx)
claimed, err := s.tryClaimAffiliateRebateAudit(txCtx, tx.Client(), o.ID, o.Amount)
if err != nil {
s.writeAuditLog(ctx, o.ID, "AFFILIATE_REBATE_FAILED", "system", map[string]any{
"error": err.Error(),
})
return
}
if !claimed {
return
}
rebateAmount, err := s.affiliateService.AccrueInviteRebate(txCtx, o.UserID, o.Amount)
if err != nil {
s.writeAuditLog(ctx, o.ID, "AFFILIATE_REBATE_FAILED", "system", map[string]any{
"error": err.Error(),
})
return
}
if rebateAmount <= 0 {
if err := s.updateClaimedAffiliateRebateAudit(txCtx, tx.Client(), o.ID, "AFFILIATE_REBATE_SKIPPED", map[string]any{
"baseAmount": o.Amount,
"reason": "no inviter bound or rebate amount <= 0",
}); err != nil {
s.writeAuditLog(ctx, o.ID, "AFFILIATE_REBATE_FAILED", "system", map[string]any{
"error": err.Error(),
})
return
}
if err := tx.Commit(); err != nil {
s.writeAuditLog(ctx, o.ID, "AFFILIATE_REBATE_FAILED", "system", map[string]any{
"error": fmt.Sprintf("commit affiliate rebate tx: %v", err),
})
}
return
}
if err := s.updateClaimedAffiliateRebateAudit(txCtx, tx.Client(), o.ID, "AFFILIATE_REBATE_APPLIED", map[string]any{
"baseAmount": o.Amount,
"rebateAmount": rebateAmount,
}); err != nil {
s.writeAuditLog(ctx, o.ID, "AFFILIATE_REBATE_FAILED", "system", map[string]any{
"error": err.Error(),
})
return
}
if err := tx.Commit(); err != nil {
s.writeAuditLog(ctx, o.ID, "AFFILIATE_REBATE_FAILED", "system", map[string]any{
"error": fmt.Sprintf("commit affiliate rebate tx: %v", err),
})
}
}
func (s *PaymentService) tryClaimAffiliateRebateAudit(ctx context.Context, client *dbent.Client, orderID int64, baseAmount float64) (bool, error) {
if client == nil {
return false, errors.New("nil payment client")
}
oid := strconv.FormatInt(orderID, 10)
detail, _ := json.Marshal(map[string]any{
"baseAmount": baseAmount,
"status": "reserved",
})
rows, err := client.QueryContext(ctx, `
INSERT INTO payment_audit_logs (order_id, action, detail, operator, created_at)
SELECT $1, 'AFFILIATE_REBATE_APPLIED', $2, 'system', NOW()
WHERE NOT EXISTS (
SELECT 1
FROM payment_audit_logs
WHERE order_id = $1
AND action IN ('AFFILIATE_REBATE_APPLIED', 'AFFILIATE_REBATE_SKIPPED')
)
ON CONFLICT (order_id, action) DO NOTHING
RETURNING id`, oid, string(detail))
if err != nil {
return false, err
}
defer func() { _ = rows.Close() }()
if !rows.Next() {
if err := rows.Err(); err != nil {
return false, err
}
return false, nil
}
var claimID int64
if err := rows.Scan(&claimID); err != nil {
return false, err
}
return true, nil
}
func (s *PaymentService) updateClaimedAffiliateRebateAudit(ctx context.Context, client *dbent.Client, orderID int64, action string, detail map[string]any) error {
if client == nil {
return errors.New("nil payment client")
}
oid := strconv.FormatInt(orderID, 10)
detailJSON, _ := json.Marshal(detail)
updated, err := client.PaymentAuditLog.Update().
Where(
paymentauditlog.OrderIDEQ(oid),
paymentauditlog.ActionEQ("AFFILIATE_REBATE_APPLIED"),
).
SetAction(action).
SetDetail(string(detailJSON)).
SetOperator("system").
Save(ctx)
if err != nil {
return err
}
if updated == 0 {
return errors.New("affiliate rebate claim log not found")
}
return nil
}
func (s *PaymentService) markFailed(ctx context.Context, oid int64, cause error) {
now := time.Now()
r := psErrMsg(cause)

View File

@@ -170,17 +170,18 @@ type TopUserStat struct {
// --- Service ---
type PaymentService struct {
providerMu sync.Mutex
providersLoaded bool
entClient *dbent.Client
registry *payment.Registry
loadBalancer payment.LoadBalancer
redeemService *RedeemService
subscriptionSvc *SubscriptionService
configService *PaymentConfigService
userRepo UserRepository
groupRepo GroupRepository
resumeService *PaymentResumeService
providerMu sync.Mutex
providersLoaded bool
entClient *dbent.Client
registry *payment.Registry
loadBalancer payment.LoadBalancer
redeemService *RedeemService
subscriptionSvc *SubscriptionService
configService *PaymentConfigService
userRepo UserRepository
groupRepo GroupRepository
resumeService *PaymentResumeService
affiliateService *AffiliateService
}
func NewPaymentService(entClient *dbent.Client, registry *payment.Registry, loadBalancer payment.LoadBalancer, redeemService *RedeemService, subscriptionSvc *SubscriptionService, configService *PaymentConfigService, userRepo UserRepository, groupRepo GroupRepository) *PaymentService {
@@ -189,6 +190,13 @@ func NewPaymentService(entClient *dbent.Client, registry *payment.Registry, load
return svc
}
func (s *PaymentService) SetAffiliateService(affiliateService *AffiliateService) {
if s == nil {
return
}
s.affiliateService = affiliateService
}
// --- Provider Registry ---
// EnsureProviders lazily initializes the provider registry on first call.

View File

@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"log/slog"
"math"
"net/url"
"sort"
"strconv"
@@ -1167,6 +1168,8 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
// 默认配置
updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
updates[SettingKeyDefaultBalance] = strconv.FormatFloat(settings.DefaultBalance, 'f', 8, 64)
settings.AffiliateRebateRate = clampAffiliateRebateRate(settings.AffiliateRebateRate)
updates[SettingKeyAffiliateRebateRate] = strconv.FormatFloat(settings.AffiliateRebateRate, 'f', 8, 64)
updates[SettingKeyDefaultUserRPMLimit] = strconv.Itoa(settings.DefaultUserRPMLimit)
defaultSubsJSON, err := json.Marshal(settings.DefaultSubscriptions)
if err != nil {
@@ -1719,6 +1722,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyOIDCConnectUserInfoUsernamePath: "",
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
SettingKeyAffiliateRebateRate: strconv.FormatFloat(AffiliateRebateRateDefault, 'f', 8, 64),
SettingKeyDefaultUserRPMLimit: "0",
SettingKeyDefaultSubscriptions: "[]",
SettingKeyAuthSourceDefaultEmailBalance: "0",
@@ -1846,6 +1850,11 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
} else {
result.DefaultBalance = s.cfg.Default.UserBalance
}
if rebateRate, err := strconv.ParseFloat(settings[SettingKeyAffiliateRebateRate], 64); err == nil {
result.AffiliateRebateRate = clampAffiliateRebateRate(rebateRate)
} else {
result.AffiliateRebateRate = AffiliateRebateRateDefault
}
result.DefaultSubscriptions = parseDefaultSubscriptions(settings[SettingKeyDefaultSubscriptions])
// 敏感信息直接返回,方便测试连接时使用
@@ -2130,6 +2139,19 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
return result
}
func clampAffiliateRebateRate(value float64) float64 {
if math.IsNaN(value) || math.IsInf(value, 0) {
return AffiliateRebateRateDefault
}
if value < AffiliateRebateRateMin {
return AffiliateRebateRateMin
}
if value > AffiliateRebateRateMax {
return AffiliateRebateRateMax
}
return value
}
func isFalseSettingValue(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "false", "0", "off", "disabled":

View File

@@ -106,6 +106,7 @@ type SystemSettings struct {
DefaultConcurrency int
DefaultBalance float64
AffiliateRebateRate float64
DefaultUserRPMLimit int
DefaultSubscriptions []DefaultSubscriptionSetting

View File

@@ -391,6 +391,53 @@ func ProvideSettingService(settingRepo SettingRepository, groupRepo GroupReposit
return svc
}
func ProvideAuthService(
entClient *dbent.Client,
userRepo UserRepository,
redeemRepo RedeemCodeRepository,
refreshTokenCache RefreshTokenCache,
cfg *config.Config,
settingService *SettingService,
emailService *EmailService,
turnstileService *TurnstileService,
emailQueueService *EmailQueueService,
promoService *PromoService,
defaultSubAssigner DefaultSubscriptionAssigner,
affiliateService *AffiliateService,
) *AuthService {
svc := NewAuthService(
entClient,
userRepo,
redeemRepo,
refreshTokenCache,
cfg,
settingService,
emailService,
turnstileService,
emailQueueService,
promoService,
defaultSubAssigner,
)
svc.SetAffiliateService(affiliateService)
return svc
}
func ProvidePaymentService(
entClient *dbent.Client,
registry *payment.Registry,
loadBalancer payment.LoadBalancer,
redeemService *RedeemService,
subscriptionSvc *SubscriptionService,
configService *PaymentConfigService,
userRepo UserRepository,
groupRepo GroupRepository,
affiliateService *AffiliateService,
) *PaymentService {
svc := NewPaymentService(entClient, registry, loadBalancer, redeemService, subscriptionSvc, configService, userRepo, groupRepo)
svc.SetAffiliateService(affiliateService)
return svc
}
// ProvideBillingCacheService wires BillingCacheService with its RPM dependencies.
func ProvideBillingCacheService(
cache BillingCache,
@@ -407,7 +454,7 @@ func ProvideBillingCacheService(
// ProviderSet is the Wire provider set for all services
var ProviderSet = wire.NewSet(
// Core services
NewAuthService,
ProvideAuthService,
NewUserService,
NewAPIKeyService,
ProvideAPIKeyAuthCacheInvalidator,
@@ -486,8 +533,9 @@ var ProviderSet = wire.NewSet(
NewGroupCapacityService,
NewChannelService,
NewModelPricingResolver,
NewAffiliateService,
ProvidePaymentConfigService,
NewPaymentService,
ProvidePaymentService,
ProvidePaymentOrderExpiryService,
ProvideBalanceNotifyService,
ProvideChannelMonitorService,