Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
f03de00c
"vscode:/vscode.git/clone" did not exist on "11dc6777c39648808c5207c324f99b7879e7768e"
Commit
f03de00c
authored
Apr 24, 2026
by
VpSanta33
Browse files
feat: add affiliate invite rebate flow and admin rebate-rate setting
parent
d162604f
Changes
33
Show whitespace changes
Inline
Side-by-side
backend/cmd/server/wire_gen.go
View file @
f03de00c
...
@@ -69,7 +69,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -69,7 +69,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
apiKeyAuthCacheInvalidator
:=
service
.
ProvideAPIKeyAuthCacheInvalidator
(
apiKeyService
)
apiKeyAuthCacheInvalidator
:=
service
.
ProvideAPIKeyAuthCacheInvalidator
(
apiKeyService
)
promoService
:=
service
.
NewPromoService
(
promoCodeRepository
,
userRepository
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
promoService
:=
service
.
NewPromoService
(
promoCodeRepository
,
userRepository
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepository
,
userSubscriptionRepository
,
billingCacheService
,
client
,
configConfig
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepository
,
userSubscriptionRepository
,
billingCacheService
,
client
,
configConfig
)
authService
:=
service
.
NewAuthService
(
client
,
userRepository
,
redeemCodeRepository
,
refreshTokenCache
,
configConfig
,
settingService
,
emailService
,
turnstileService
,
emailQueueService
,
promoService
,
subscriptionService
)
affiliateRepository
:=
repository
.
NewAffiliateRepository
(
client
,
db
)
affiliateService
:=
service
.
NewAffiliateService
(
affiliateRepository
,
settingRepository
,
apiKeyAuthCacheInvalidator
,
billingCacheService
)
authService
:=
service
.
ProvideAuthService
(
client
,
userRepository
,
redeemCodeRepository
,
refreshTokenCache
,
configConfig
,
settingService
,
emailService
,
turnstileService
,
emailQueueService
,
promoService
,
subscriptionService
,
affiliateService
)
userService
:=
service
.
NewUserService
(
userRepository
,
settingRepository
,
apiKeyAuthCacheInvalidator
,
billingCache
)
userService
:=
service
.
NewUserService
(
userRepository
,
settingRepository
,
apiKeyAuthCacheInvalidator
,
billingCache
)
redeemCache
:=
repository
.
NewRedeemCache
(
redisClient
)
redeemCache
:=
repository
.
NewRedeemCache
(
redisClient
)
redeemService
:=
service
.
NewRedeemService
(
redeemCodeRepository
,
userRepository
,
subscriptionService
,
redeemCache
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
redeemService
:=
service
.
NewRedeemService
(
redeemCodeRepository
,
userRepository
,
subscriptionService
,
redeemCache
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
...
@@ -80,7 +82,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -80,7 +82,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
totpCache
:=
repository
.
NewTotpCache
(
redisClient
)
totpCache
:=
repository
.
NewTotpCache
(
redisClient
)
totpService
:=
service
.
NewTotpService
(
userRepository
,
secretEncryptor
,
totpCache
,
settingService
,
emailService
,
emailQueueService
)
totpService
:=
service
.
NewTotpService
(
userRepository
,
secretEncryptor
,
totpCache
,
settingService
,
emailService
,
emailQueueService
)
authHandler
:=
handler
.
NewAuthHandler
(
configConfig
,
authService
,
userService
,
settingService
,
promoService
,
redeemService
,
totpService
)
authHandler
:=
handler
.
NewAuthHandler
(
configConfig
,
authService
,
userService
,
settingService
,
promoService
,
redeemService
,
totpService
)
userHandler
:=
handler
.
New
UserHandler
(
userService
,
authService
,
emailService
,
emailCache
)
userHandler
:=
handler
.
Provide
UserHandler
(
userService
,
authService
,
emailService
,
emailCache
,
affiliateService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
...
@@ -91,6 +93,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -91,6 +93,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
announcementReadRepository
:=
repository
.
NewAnnouncementReadRepository
(
client
)
announcementReadRepository
:=
repository
.
NewAnnouncementReadRepository
(
client
)
announcementService
:=
service
.
NewAnnouncementService
(
announcementRepository
,
announcementReadRepository
,
userRepository
,
userSubscriptionRepository
)
announcementService
:=
service
.
NewAnnouncementService
(
announcementRepository
,
announcementReadRepository
,
userRepository
,
userSubscriptionRepository
)
announcementHandler
:=
handler
.
NewAnnouncementHandler
(
announcementService
)
announcementHandler
:=
handler
.
NewAnnouncementHandler
(
announcementService
)
channelMonitorRepository
:=
repository
.
NewChannelMonitorRepository
(
client
,
db
)
channelMonitorService
:=
service
.
ProvideChannelMonitorService
(
channelMonitorRepository
,
secretEncryptor
)
channelMonitorUserHandler
:=
handler
.
NewChannelMonitorUserHandler
(
channelMonitorService
,
settingService
)
dashboardAggregationRepository
:=
repository
.
NewDashboardAggregationRepository
(
db
)
dashboardAggregationRepository
:=
repository
.
NewDashboardAggregationRepository
(
db
)
dashboardStatsCache
:=
repository
.
NewDashboardCache
(
redisClient
,
configConfig
)
dashboardStatsCache
:=
repository
.
NewDashboardCache
(
redisClient
,
configConfig
)
dashboardService
:=
service
.
NewDashboardService
(
usageLogRepository
,
dashboardAggregationRepository
,
dashboardStatsCache
,
configConfig
)
dashboardService
:=
service
.
NewDashboardService
(
usageLogRepository
,
dashboardAggregationRepository
,
dashboardStatsCache
,
configConfig
)
...
@@ -192,7 +197,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -192,7 +197,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
paymentConfigService
:=
service
.
ProvidePaymentConfigService
(
client
,
settingRepository
,
encryptionKey
)
paymentConfigService
:=
service
.
ProvidePaymentConfigService
(
client
,
settingRepository
,
encryptionKey
)
registry
:=
payment
.
ProvideRegistry
()
registry
:=
payment
.
ProvideRegistry
()
defaultLoadBalancer
:=
payment
.
ProvideDefaultLoadBalancer
(
client
,
encryptionKey
)
defaultLoadBalancer
:=
payment
.
ProvideDefaultLoadBalancer
(
client
,
encryptionKey
)
paymentService
:=
service
.
New
PaymentService
(
client
,
registry
,
defaultLoadBalancer
,
redeemService
,
subscriptionService
,
paymentConfigService
,
userRepository
,
groupRepository
)
paymentService
:=
service
.
Provide
PaymentService
(
client
,
registry
,
defaultLoadBalancer
,
redeemService
,
subscriptionService
,
paymentConfigService
,
userRepository
,
groupRepository
,
affiliateService
)
settingHandler
:=
admin
.
NewSettingHandler
(
settingService
,
emailService
,
turnstileService
,
opsService
,
paymentConfigService
,
paymentService
)
settingHandler
:=
admin
.
NewSettingHandler
(
settingService
,
emailService
,
turnstileService
,
opsService
,
paymentConfigService
,
paymentService
)
opsHandler
:=
admin
.
NewOpsHandler
(
opsService
)
opsHandler
:=
admin
.
NewOpsHandler
(
opsService
)
updateCache
:=
repository
.
NewUpdateCache
(
redisClient
)
updateCache
:=
repository
.
NewUpdateCache
(
redisClient
)
...
@@ -221,20 +226,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -221,20 +226,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
scheduledTestService
:=
service
.
ProvideScheduledTestService
(
scheduledTestPlanRepository
,
scheduledTestResultRepository
)
scheduledTestService
:=
service
.
ProvideScheduledTestService
(
scheduledTestPlanRepository
,
scheduledTestResultRepository
)
scheduledTestHandler
:=
admin
.
NewScheduledTestHandler
(
scheduledTestService
)
scheduledTestHandler
:=
admin
.
NewScheduledTestHandler
(
scheduledTestService
)
channelHandler
:=
admin
.
NewChannelHandler
(
channelService
,
billingService
)
channelHandler
:=
admin
.
NewChannelHandler
(
channelService
,
billingService
)
sqlDB
,
err
:=
repository
.
ProvideSQLDB
(
client
)
channelMonitorHandler
:=
admin
.
NewChannelMonitorHandler
(
channelMonitorService
)
if
err
!=
nil
{
channelMonitorRequestTemplateRepository
:=
repository
.
NewChannelMonitorRequestTemplateRepository
(
client
,
db
)
return
nil
,
err
}
channelMonitorRepository
:=
repository
.
NewChannelMonitorRepository
(
client
,
sqlDB
)
channelMonitorRequestTemplateRepository
:=
repository
.
NewChannelMonitorRequestTemplateRepository
(
client
,
sqlDB
)
channelMonitorRequestTemplateService
:=
service
.
NewChannelMonitorRequestTemplateService
(
channelMonitorRequestTemplateRepository
)
channelMonitorRequestTemplateService
:=
service
.
NewChannelMonitorRequestTemplateService
(
channelMonitorRequestTemplateRepository
)
channelMonitorRequestTemplateHandler
:=
admin
.
NewChannelMonitorRequestTemplateHandler
(
channelMonitorRequestTemplateService
)
channelMonitorRequestTemplateHandler
:=
admin
.
NewChannelMonitorRequestTemplateHandler
(
channelMonitorRequestTemplateService
)
channelMonitorService
:=
service
.
ProvideChannelMonitorService
(
channelMonitorRepository
,
secretEncryptor
)
channelMonitorHandler
:=
admin
.
NewChannelMonitorHandler
(
channelMonitorService
)
channelMonitorUserHandler
:=
handler
.
NewChannelMonitorUserHandler
(
channelMonitorService
,
settingService
)
channelMonitorRunner
:=
service
.
ProvideChannelMonitorRunner
(
channelMonitorService
,
settingService
)
paymentHandler
:=
admin
.
NewPaymentHandler
(
paymentService
,
paymentConfigService
)
paymentHandler
:=
admin
.
NewPaymentHandler
(
paymentService
,
paymentConfigService
)
availableChannelUserHandler
:=
handler
.
NewAvailableChannelHandler
(
channelService
,
apiKeyService
,
settingService
)
adminHandlers
:=
handler
.
ProvideAdminHandlers
(
dashboardHandler
,
adminUserHandler
,
groupHandler
,
accountHandler
,
adminAnnouncementHandler
,
dataManagementHandler
,
backupHandler
,
oAuthHandler
,
openAIOAuthHandler
,
geminiOAuthHandler
,
antigravityOAuthHandler
,
proxyHandler
,
adminRedeemHandler
,
promoHandler
,
settingHandler
,
opsHandler
,
systemHandler
,
adminSubscriptionHandler
,
adminUsageHandler
,
userAttributeHandler
,
errorPassthroughHandler
,
tlsFingerprintProfileHandler
,
adminAPIKeyHandler
,
scheduledTestHandler
,
channelHandler
,
channelMonitorHandler
,
channelMonitorRequestTemplateHandler
,
paymentHandler
)
adminHandlers
:=
handler
.
ProvideAdminHandlers
(
dashboardHandler
,
adminUserHandler
,
groupHandler
,
accountHandler
,
adminAnnouncementHandler
,
dataManagementHandler
,
backupHandler
,
oAuthHandler
,
openAIOAuthHandler
,
geminiOAuthHandler
,
antigravityOAuthHandler
,
proxyHandler
,
adminRedeemHandler
,
promoHandler
,
settingHandler
,
opsHandler
,
systemHandler
,
adminSubscriptionHandler
,
adminUsageHandler
,
userAttributeHandler
,
errorPassthroughHandler
,
tlsFingerprintProfileHandler
,
adminAPIKeyHandler
,
scheduledTestHandler
,
channelHandler
,
channelMonitorHandler
,
channelMonitorRequestTemplateHandler
,
paymentHandler
)
usageRecordWorkerPool
:=
service
.
NewUsageRecordWorkerPool
(
configConfig
)
usageRecordWorkerPool
:=
service
.
NewUsageRecordWorkerPool
(
configConfig
)
userMsgQueueCache
:=
repository
.
NewUserMsgQueueCache
(
redisClient
)
userMsgQueueCache
:=
repository
.
NewUserMsgQueueCache
(
redisClient
)
...
@@ -245,9 +241,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -245,9 +241,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
totpHandler
:=
handler
.
NewTotpHandler
(
totpService
)
totpHandler
:=
handler
.
NewTotpHandler
(
totpService
)
handlerPaymentHandler
:=
handler
.
NewPaymentHandler
(
paymentService
,
paymentConfigService
,
channelService
)
handlerPaymentHandler
:=
handler
.
NewPaymentHandler
(
paymentService
,
paymentConfigService
,
channelService
)
paymentWebhookHandler
:=
handler
.
NewPaymentWebhookHandler
(
paymentService
,
registry
)
paymentWebhookHandler
:=
handler
.
NewPaymentWebhookHandler
(
paymentService
,
registry
)
availableChannelHandler
:=
handler
.
NewAvailableChannelHandler
(
channelService
,
apiKeyService
,
settingService
)
idempotencyCoordinator
:=
service
.
ProvideIdempotencyCoordinator
(
idempotencyRepository
,
configConfig
)
idempotencyCoordinator
:=
service
.
ProvideIdempotencyCoordinator
(
idempotencyRepository
,
configConfig
)
idempotencyCleanupService
:=
service
.
ProvideIdempotencyCleanupService
(
idempotencyRepository
,
configConfig
)
idempotencyCleanupService
:=
service
.
ProvideIdempotencyCleanupService
(
idempotencyRepository
,
configConfig
)
handlers
:=
handler
.
ProvideHandlers
(
authHandler
,
userHandler
,
apiKeyHandler
,
usageHandler
,
redeemHandler
,
subscriptionHandler
,
announcementHandler
,
channelMonitorUserHandler
,
adminHandlers
,
gatewayHandler
,
openAIGatewayHandler
,
handlerSettingHandler
,
totpHandler
,
handlerPaymentHandler
,
paymentWebhookHandler
,
availableChannel
User
Handler
,
idempotencyCoordinator
,
idempotencyCleanupService
)
handlers
:=
handler
.
ProvideHandlers
(
authHandler
,
userHandler
,
apiKeyHandler
,
usageHandler
,
redeemHandler
,
subscriptionHandler
,
announcementHandler
,
channelMonitorUserHandler
,
adminHandlers
,
gatewayHandler
,
openAIGatewayHandler
,
handlerSettingHandler
,
totpHandler
,
handlerPaymentHandler
,
paymentWebhookHandler
,
availableChannelHandler
,
idempotencyCoordinator
,
idempotencyCleanupService
)
jwtAuthMiddleware
:=
middleware
.
NewJWTAuthMiddleware
(
authService
,
userService
)
jwtAuthMiddleware
:=
middleware
.
NewJWTAuthMiddleware
(
authService
,
userService
)
adminAuthMiddleware
:=
middleware
.
NewAdminAuthMiddleware
(
authService
,
userService
,
settingService
)
adminAuthMiddleware
:=
middleware
.
NewAdminAuthMiddleware
(
authService
,
userService
,
settingService
)
apiKeyAuthMiddleware
:=
middleware
.
NewAPIKeyAuthMiddleware
(
apiKeyService
,
subscriptionService
,
configConfig
)
apiKeyAuthMiddleware
:=
middleware
.
NewAPIKeyAuthMiddleware
(
apiKeyService
,
subscriptionService
,
configConfig
)
...
@@ -263,6 +260,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -263,6 +260,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
subscriptionExpiryService
:=
service
.
ProvideSubscriptionExpiryService
(
userSubscriptionRepository
)
subscriptionExpiryService
:=
service
.
ProvideSubscriptionExpiryService
(
userSubscriptionRepository
)
scheduledTestRunnerService
:=
service
.
ProvideScheduledTestRunnerService
(
scheduledTestPlanRepository
,
scheduledTestService
,
accountTestService
,
rateLimitService
,
configConfig
)
scheduledTestRunnerService
:=
service
.
ProvideScheduledTestRunnerService
(
scheduledTestPlanRepository
,
scheduledTestService
,
accountTestService
,
rateLimitService
,
configConfig
)
paymentOrderExpiryService
:=
service
.
ProvidePaymentOrderExpiryService
(
paymentService
)
paymentOrderExpiryService
:=
service
.
ProvidePaymentOrderExpiryService
(
paymentService
)
channelMonitorRunner
:=
service
.
ProvideChannelMonitorRunner
(
channelMonitorService
,
settingService
)
v
:=
provideCleanup
(
client
,
redisClient
,
opsMetricsCollector
,
opsAggregationService
,
opsAlertEvaluatorService
,
opsCleanupService
,
opsScheduledReportService
,
opsSystemLogSink
,
schedulerSnapshotService
,
tokenRefreshService
,
accountExpiryService
,
subscriptionExpiryService
,
usageCleanupService
,
idempotencyCleanupService
,
pricingService
,
emailQueueService
,
billingCacheService
,
usageRecordWorkerPool
,
subscriptionService
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
,
antigravityOAuthService
,
openAIGatewayService
,
scheduledTestRunnerService
,
backupService
,
paymentOrderExpiryService
,
channelMonitorRunner
)
v
:=
provideCleanup
(
client
,
redisClient
,
opsMetricsCollector
,
opsAggregationService
,
opsAlertEvaluatorService
,
opsCleanupService
,
opsScheduledReportService
,
opsSystemLogSink
,
schedulerSnapshotService
,
tokenRefreshService
,
accountExpiryService
,
subscriptionExpiryService
,
usageCleanupService
,
idempotencyCleanupService
,
pricingService
,
emailQueueService
,
billingCacheService
,
usageRecordWorkerPool
,
subscriptionService
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
,
antigravityOAuthService
,
openAIGatewayService
,
scheduledTestRunnerService
,
backupService
,
paymentOrderExpiryService
,
channelMonitorRunner
)
application
:=
&
Application
{
application
:=
&
Application
{
Server
:
httpServer
,
Server
:
httpServer
,
...
...
backend/internal/handler/admin/setting_handler.go
View file @
f03de00c
...
@@ -185,6 +185,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
...
@@ -185,6 +185,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
DefaultConcurrency
:
settings
.
DefaultConcurrency
,
DefaultConcurrency
:
settings
.
DefaultConcurrency
,
DefaultBalance
:
settings
.
DefaultBalance
,
DefaultBalance
:
settings
.
DefaultBalance
,
AffiliateRebateRate
:
settings
.
AffiliateRebateRate
,
DefaultUserRPMLimit
:
settings
.
DefaultUserRPMLimit
,
DefaultUserRPMLimit
:
settings
.
DefaultUserRPMLimit
,
DefaultSubscriptions
:
defaultSubscriptions
,
DefaultSubscriptions
:
defaultSubscriptions
,
EnableModelFallback
:
settings
.
EnableModelFallback
,
EnableModelFallback
:
settings
.
EnableModelFallback
,
...
@@ -338,6 +339,7 @@ type UpdateSettingsRequest struct {
...
@@ -338,6 +339,7 @@ type UpdateSettingsRequest struct {
// 默认配置
// 默认配置
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultBalance
float64
`json:"default_balance"`
DefaultBalance
float64
`json:"default_balance"`
AffiliateRebateRate
*
float64
`json:"affiliate_rebate_rate"`
DefaultUserRPMLimit
int
`json:"default_user_rpm_limit"`
DefaultUserRPMLimit
int
`json:"default_user_rpm_limit"`
DefaultSubscriptions
[]
dto
.
DefaultSubscriptionSetting
`json:"default_subscriptions"`
DefaultSubscriptions
[]
dto
.
DefaultSubscriptionSetting
`json:"default_subscriptions"`
AuthSourceDefaultEmailBalance
*
float64
`json:"auth_source_default_email_balance"`
AuthSourceDefaultEmailBalance
*
float64
`json:"auth_source_default_email_balance"`
...
@@ -468,6 +470,16 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -468,6 +470,16 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
if
req
.
DefaultBalance
<
0
{
if
req
.
DefaultBalance
<
0
{
req
.
DefaultBalance
=
0
req
.
DefaultBalance
=
0
}
}
affiliateRebateRate
:=
previousSettings
.
AffiliateRebateRate
if
req
.
AffiliateRebateRate
!=
nil
{
affiliateRebateRate
=
*
req
.
AffiliateRebateRate
}
if
affiliateRebateRate
<
service
.
AffiliateRebateRateMin
{
affiliateRebateRate
=
service
.
AffiliateRebateRateMin
}
if
affiliateRebateRate
>
service
.
AffiliateRebateRateMax
{
affiliateRebateRate
=
service
.
AffiliateRebateRateMax
}
// 通用表格配置:兼容旧客户端未传字段时保留当前值。
// 通用表格配置:兼容旧客户端未传字段时保留当前值。
if
req
.
TableDefaultPageSize
<=
0
{
if
req
.
TableDefaultPageSize
<=
0
{
req
.
TableDefaultPageSize
=
previousSettings
.
TableDefaultPageSize
req
.
TableDefaultPageSize
=
previousSettings
.
TableDefaultPageSize
...
@@ -1119,6 +1131,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -1119,6 +1131,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
CustomEndpoints
:
customEndpointsJSON
,
CustomEndpoints
:
customEndpointsJSON
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultBalance
:
req
.
DefaultBalance
,
DefaultBalance
:
req
.
DefaultBalance
,
AffiliateRebateRate
:
affiliateRebateRate
,
DefaultUserRPMLimit
:
req
.
DefaultUserRPMLimit
,
DefaultUserRPMLimit
:
req
.
DefaultUserRPMLimit
,
DefaultSubscriptions
:
defaultSubscriptions
,
DefaultSubscriptions
:
defaultSubscriptions
,
EnableModelFallback
:
req
.
EnableModelFallback
,
EnableModelFallback
:
req
.
EnableModelFallback
,
...
@@ -1433,6 +1446,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -1433,6 +1446,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
updatedSettings
.
CustomEndpoints
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
updatedSettings
.
CustomEndpoints
),
DefaultConcurrency
:
updatedSettings
.
DefaultConcurrency
,
DefaultConcurrency
:
updatedSettings
.
DefaultConcurrency
,
DefaultBalance
:
updatedSettings
.
DefaultBalance
,
DefaultBalance
:
updatedSettings
.
DefaultBalance
,
AffiliateRebateRate
:
updatedSettings
.
AffiliateRebateRate
,
DefaultUserRPMLimit
:
updatedSettings
.
DefaultUserRPMLimit
,
DefaultUserRPMLimit
:
updatedSettings
.
DefaultUserRPMLimit
,
DefaultSubscriptions
:
updatedDefaultSubscriptions
,
DefaultSubscriptions
:
updatedDefaultSubscriptions
,
EnableModelFallback
:
updatedSettings
.
EnableModelFallback
,
EnableModelFallback
:
updatedSettings
.
EnableModelFallback
,
...
@@ -1738,6 +1752,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
...
@@ -1738,6 +1752,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
DefaultBalance
!=
after
.
DefaultBalance
{
if
before
.
DefaultBalance
!=
after
.
DefaultBalance
{
changed
=
append
(
changed
,
"default_balance"
)
changed
=
append
(
changed
,
"default_balance"
)
}
}
if
before
.
AffiliateRebateRate
!=
after
.
AffiliateRebateRate
{
changed
=
append
(
changed
,
"affiliate_rebate_rate"
)
}
if
!
equalDefaultSubscriptions
(
before
.
DefaultSubscriptions
,
after
.
DefaultSubscriptions
)
{
if
!
equalDefaultSubscriptions
(
before
.
DefaultSubscriptions
,
after
.
DefaultSubscriptions
)
{
changed
=
append
(
changed
,
"default_subscriptions"
)
changed
=
append
(
changed
,
"default_subscriptions"
)
}
}
...
...
backend/internal/handler/auth_handler.go
View file @
f03de00c
...
@@ -48,6 +48,7 @@ type RegisterRequest struct {
...
@@ -48,6 +48,7 @@ type RegisterRequest struct {
TurnstileToken
string
`json:"turnstile_token"`
TurnstileToken
string
`json:"turnstile_token"`
PromoCode
string
`json:"promo_code"`
// 注册优惠码
PromoCode
string
`json:"promo_code"`
// 注册优惠码
InvitationCode
string
`json:"invitation_code"`
// 邀请码
InvitationCode
string
`json:"invitation_code"`
// 邀请码
AffCode
string
`json:"aff_code"`
// 邀请返利码
}
}
// SendVerifyCodeRequest 发送验证码请求
// SendVerifyCodeRequest 发送验证码请求
...
@@ -164,7 +165,15 @@ func (h *AuthHandler) Register(c *gin.Context) {
...
@@ -164,7 +165,15 @@ func (h *AuthHandler) Register(c *gin.Context) {
return
return
}
}
_
,
user
,
err
:=
h
.
authService
.
RegisterWithVerification
(
c
.
Request
.
Context
(),
req
.
Email
,
req
.
Password
,
req
.
VerifyCode
,
req
.
PromoCode
,
req
.
InvitationCode
)
_
,
user
,
err
:=
h
.
authService
.
RegisterWithVerification
(
c
.
Request
.
Context
(),
req
.
Email
,
req
.
Password
,
req
.
VerifyCode
,
req
.
PromoCode
,
req
.
InvitationCode
,
req
.
AffCode
,
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
...
backend/internal/handler/dto/settings.go
View file @
f03de00c
...
@@ -108,6 +108,7 @@ type SystemSettings struct {
...
@@ -108,6 +108,7 @@ type SystemSettings struct {
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultBalance
float64
`json:"default_balance"`
DefaultBalance
float64
`json:"default_balance"`
AffiliateRebateRate
float64
`json:"affiliate_rebate_rate"`
DefaultUserRPMLimit
int
`json:"default_user_rpm_limit"`
DefaultUserRPMLimit
int
`json:"default_user_rpm_limit"`
DefaultSubscriptions
[]
DefaultSubscriptionSetting
`json:"default_subscriptions"`
DefaultSubscriptions
[]
DefaultSubscriptionSetting
`json:"default_subscriptions"`
...
...
backend/internal/handler/user_handler.go
View file @
f03de00c
...
@@ -5,6 +5,7 @@ import (
...
@@ -5,6 +5,7 @@ import (
"strings"
"strings"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
middleware2
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
middleware2
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/Wei-Shaw/sub2api/internal/service"
...
@@ -18,6 +19,7 @@ type UserHandler struct {
...
@@ -18,6 +19,7 @@ type UserHandler struct {
authService
*
service
.
AuthService
authService
*
service
.
AuthService
emailService
*
service
.
EmailService
emailService
*
service
.
EmailService
emailCache
service
.
EmailCache
emailCache
service
.
EmailCache
affiliateService
*
service
.
AffiliateService
}
}
// NewUserHandler creates a new UserHandler
// NewUserHandler creates a new UserHandler
...
@@ -35,6 +37,13 @@ func NewUserHandler(
...
@@ -35,6 +37,13 @@ func NewUserHandler(
}
}
}
}
func
(
h
*
UserHandler
)
SetAffiliateService
(
affiliateService
*
service
.
AffiliateService
)
{
if
h
==
nil
{
return
}
h
.
affiliateService
=
affiliateService
}
// ChangePasswordRequest represents the change password request payload
// ChangePasswordRequest represents the change password request payload
type
ChangePasswordRequest
struct
{
type
ChangePasswordRequest
struct
{
OldPassword
string
`json:"old_password" binding:"required"`
OldPassword
string
`json:"old_password" binding:"required"`
...
@@ -159,6 +168,63 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
...
@@ -159,6 +168,63 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
response
.
Success
(
c
,
profileResp
)
response
.
Success
(
c
,
profileResp
)
}
}
func
(
h
*
UserHandler
)
affiliateServiceOrErr
()
(
*
service
.
AffiliateService
,
error
)
{
if
h
==
nil
||
h
.
affiliateService
==
nil
{
return
nil
,
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"affiliate service unavailable"
)
}
return
h
.
affiliateService
,
nil
}
// GetAffiliate returns the current user's affiliate details.
// GET /api/v1/user/aff
func
(
h
*
UserHandler
)
GetAffiliate
(
c
*
gin
.
Context
)
{
subject
,
ok
:=
middleware2
.
GetAuthSubjectFromContext
(
c
)
if
!
ok
{
response
.
Unauthorized
(
c
,
"User not authenticated"
)
return
}
affiliateSvc
,
err
:=
h
.
affiliateServiceOrErr
()
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
detail
,
err
:=
affiliateSvc
.
GetAffiliateDetail
(
c
.
Request
.
Context
(),
subject
.
UserID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
detail
)
}
// TransferAffiliateQuota transfers all available affiliate quota into current balance.
// POST /api/v1/user/aff/transfer
func
(
h
*
UserHandler
)
TransferAffiliateQuota
(
c
*
gin
.
Context
)
{
subject
,
ok
:=
middleware2
.
GetAuthSubjectFromContext
(
c
)
if
!
ok
{
response
.
Unauthorized
(
c
,
"User not authenticated"
)
return
}
affiliateSvc
,
err
:=
h
.
affiliateServiceOrErr
()
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
transferred
,
balance
,
err
:=
affiliateSvc
.
TransferAffiliateQuota
(
c
.
Request
.
Context
(),
subject
.
UserID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"transferred_quota"
:
transferred
,
"balance"
:
balance
,
})
}
type
StartIdentityBindingRequest
struct
{
type
StartIdentityBindingRequest
struct
{
Provider
string
`json:"provider" binding:"required"`
Provider
string
`json:"provider" binding:"required"`
RedirectTo
string
`json:"redirect_to"`
RedirectTo
string
`json:"redirect_to"`
...
...
backend/internal/handler/wire.go
View file @
f03de00c
...
@@ -80,6 +80,18 @@ func ProvideSettingHandler(settingService *service.SettingService, buildInfo Bui
...
@@ -80,6 +80,18 @@ func ProvideSettingHandler(settingService *service.SettingService, buildInfo Bui
return
NewSettingHandler
(
settingService
,
buildInfo
.
Version
)
return
NewSettingHandler
(
settingService
,
buildInfo
.
Version
)
}
}
func
ProvideUserHandler
(
userService
*
service
.
UserService
,
authService
*
service
.
AuthService
,
emailService
*
service
.
EmailService
,
emailCache
service
.
EmailCache
,
affiliateService
*
service
.
AffiliateService
,
)
*
UserHandler
{
handler
:=
NewUserHandler
(
userService
,
authService
,
emailService
,
emailCache
)
handler
.
SetAffiliateService
(
affiliateService
)
return
handler
}
// ProvideHandlers creates the Handlers struct
// ProvideHandlers creates the Handlers struct
func
ProvideHandlers
(
func
ProvideHandlers
(
authHandler
*
AuthHandler
,
authHandler
*
AuthHandler
,
...
@@ -125,7 +137,7 @@ func ProvideHandlers(
...
@@ -125,7 +137,7 @@ func ProvideHandlers(
var
ProviderSet
=
wire
.
NewSet
(
var
ProviderSet
=
wire
.
NewSet
(
// Top-level handlers
// Top-level handlers
NewAuthHandler
,
NewAuthHandler
,
New
UserHandler
,
Provide
UserHandler
,
NewAPIKeyHandler
,
NewAPIKeyHandler
,
NewUsageHandler
,
NewUsageHandler
,
NewRedeemHandler
,
NewRedeemHandler
,
...
...
backend/internal/repository/affiliate_repo.go
0 → 100644
View file @
f03de00c
package
repository
import
(
"context"
"crypto/rand"
"database/sql"
"errors"
"fmt"
"strings"
"time"
dbent
"github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/user"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/lib/pq"
)
const
(
affiliateCodeLength
=
12
affiliateCodeMaxAttempts
=
12
)
var
affiliateCodeCharset
=
[]
byte
(
"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
)
type
affiliateQueryExecer
interface
{
QueryContext
(
ctx
context
.
Context
,
query
string
,
args
...
any
)
(
*
sql
.
Rows
,
error
)
ExecContext
(
ctx
context
.
Context
,
query
string
,
args
...
any
)
(
sql
.
Result
,
error
)
}
type
affiliateRepository
struct
{
client
*
dbent
.
Client
}
func
NewAffiliateRepository
(
client
*
dbent
.
Client
,
_
*
sql
.
DB
)
service
.
AffiliateRepository
{
return
&
affiliateRepository
{
client
:
client
}
}
func
(
r
*
affiliateRepository
)
EnsureUserAffiliate
(
ctx
context
.
Context
,
userID
int64
)
(
*
service
.
AffiliateSummary
,
error
)
{
if
userID
<=
0
{
return
nil
,
service
.
ErrUserNotFound
}
client
:=
clientFromContext
(
ctx
,
r
.
client
)
return
ensureUserAffiliateWithClient
(
ctx
,
client
,
userID
)
}
func
(
r
*
affiliateRepository
)
GetAffiliateByCode
(
ctx
context
.
Context
,
code
string
)
(
*
service
.
AffiliateSummary
,
error
)
{
client
:=
clientFromContext
(
ctx
,
r
.
client
)
return
queryAffiliateByCode
(
ctx
,
client
,
code
)
}
func
(
r
*
affiliateRepository
)
BindInviter
(
ctx
context
.
Context
,
userID
,
inviterID
int64
)
(
bool
,
error
)
{
var
bound
bool
err
:=
r
.
withTx
(
ctx
,
func
(
txCtx
context
.
Context
,
txClient
*
dbent
.
Client
)
error
{
if
_
,
err
:=
ensureUserAffiliateWithClient
(
txCtx
,
txClient
,
userID
);
err
!=
nil
{
return
err
}
if
_
,
err
:=
ensureUserAffiliateWithClient
(
txCtx
,
txClient
,
inviterID
);
err
!=
nil
{
return
err
}
res
,
err
:=
txClient
.
ExecContext
(
txCtx
,
"UPDATE user_affiliates SET inviter_id = $1, updated_at = NOW() WHERE user_id = $2 AND inviter_id IS NULL"
,
inviterID
,
userID
,
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"bind inviter: %w"
,
err
)
}
affected
,
_
:=
res
.
RowsAffected
()
if
affected
==
0
{
bound
=
false
return
nil
}
if
_
,
err
=
txClient
.
ExecContext
(
txCtx
,
"UPDATE user_affiliates SET aff_count = aff_count + 1, updated_at = NOW() WHERE user_id = $1"
,
inviterID
,
);
err
!=
nil
{
return
fmt
.
Errorf
(
"increment inviter aff_count: %w"
,
err
)
}
bound
=
true
return
nil
})
if
err
!=
nil
{
return
false
,
err
}
return
bound
,
nil
}
func
(
r
*
affiliateRepository
)
AccrueQuota
(
ctx
context
.
Context
,
inviterID
,
inviteeUserID
int64
,
amount
float64
)
(
bool
,
error
)
{
if
amount
<=
0
{
return
false
,
nil
}
var
applied
bool
err
:=
r
.
withTx
(
ctx
,
func
(
txCtx
context
.
Context
,
txClient
*
dbent
.
Client
)
error
{
res
,
err
:=
txClient
.
ExecContext
(
txCtx
,
"UPDATE user_affiliates SET aff_quota = aff_quota + $1, aff_history_quota = aff_history_quota + $1, updated_at = NOW() WHERE user_id = $2"
,
amount
,
inviterID
,
)
if
err
!=
nil
{
return
err
}
affected
,
_
:=
res
.
RowsAffected
()
if
affected
==
0
{
applied
=
false
return
nil
}
if
_
,
err
=
txClient
.
ExecContext
(
txCtx
,
`
INSERT INTO user_affiliate_ledger (user_id, action, amount, source_user_id, created_at, updated_at)
VALUES ($1, 'accrue', $2, $3, NOW(), NOW())`
,
inviterID
,
amount
,
inviteeUserID
);
err
!=
nil
{
return
fmt
.
Errorf
(
"insert affiliate accrue ledger: %w"
,
err
)
}
applied
=
true
return
nil
})
if
err
!=
nil
{
return
false
,
err
}
return
applied
,
nil
}
func
(
r
*
affiliateRepository
)
TransferQuotaToBalance
(
ctx
context
.
Context
,
userID
int64
)
(
float64
,
float64
,
error
)
{
var
transferred
float64
var
newBalance
float64
err
:=
r
.
withTx
(
ctx
,
func
(
txCtx
context
.
Context
,
txClient
*
dbent
.
Client
)
error
{
if
_
,
err
:=
ensureUserAffiliateWithClient
(
txCtx
,
txClient
,
userID
);
err
!=
nil
{
return
err
}
rows
,
err
:=
txClient
.
QueryContext
(
txCtx
,
`
WITH claimed AS (
SELECT aff_quota::double precision AS amount
FROM user_affiliates
WHERE user_id = $1
AND aff_quota > 0
FOR UPDATE
),
cleared AS (
UPDATE user_affiliates ua
SET aff_quota = 0,
updated_at = NOW()
FROM claimed c
WHERE ua.user_id = $1
RETURNING c.amount
)
SELECT amount
FROM cleared`
,
userID
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"claim affiliate quota: %w"
,
err
)
}
if
!
rows
.
Next
()
{
_
=
rows
.
Close
()
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
err
}
return
service
.
ErrAffiliateQuotaEmpty
}
if
err
:=
rows
.
Scan
(
&
transferred
);
err
!=
nil
{
_
=
rows
.
Close
()
return
err
}
if
err
:=
rows
.
Close
();
err
!=
nil
{
return
err
}
if
transferred
<=
0
{
return
service
.
ErrAffiliateQuotaEmpty
}
affected
,
err
:=
txClient
.
User
.
Update
()
.
Where
(
user
.
IDEQ
(
userID
))
.
AddBalance
(
transferred
)
.
AddTotalRecharged
(
transferred
)
.
Save
(
txCtx
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"credit user balance by affiliate quota: %w"
,
err
)
}
if
affected
==
0
{
return
service
.
ErrUserNotFound
}
newBalance
,
err
=
queryUserBalance
(
txCtx
,
txClient
,
userID
)
if
err
!=
nil
{
return
err
}
if
_
,
err
=
txClient
.
ExecContext
(
txCtx
,
`
INSERT INTO user_affiliate_ledger (user_id, action, amount, source_user_id, created_at, updated_at)
VALUES ($1, 'transfer', $2, NULL, NOW(), NOW())`
,
userID
,
transferred
);
err
!=
nil
{
return
fmt
.
Errorf
(
"insert affiliate transfer ledger: %w"
,
err
)
}
return
nil
})
if
err
!=
nil
{
return
0
,
0
,
err
}
return
transferred
,
newBalance
,
nil
}
func
(
r
*
affiliateRepository
)
ListInvitees
(
ctx
context
.
Context
,
inviterID
int64
,
limit
int
)
([]
service
.
AffiliateInvitee
,
error
)
{
if
limit
<=
0
{
limit
=
100
}
client
:=
clientFromContext
(
ctx
,
r
.
client
)
rows
,
err
:=
client
.
QueryContext
(
ctx
,
`
SELECT ua.user_id,
COALESCE(u.email, ''),
COALESCE(u.username, ''),
ua.created_at
FROM user_affiliates ua
LEFT JOIN users u ON u.id = ua.user_id
WHERE ua.inviter_id = $1
ORDER BY ua.created_at DESC
LIMIT $2`
,
inviterID
,
limit
)
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
invitees
:=
make
([]
service
.
AffiliateInvitee
,
0
)
for
rows
.
Next
()
{
var
item
service
.
AffiliateInvitee
var
createdAt
time
.
Time
if
err
:=
rows
.
Scan
(
&
item
.
UserID
,
&
item
.
Email
,
&
item
.
Username
,
&
createdAt
);
err
!=
nil
{
return
nil
,
err
}
item
.
CreatedAt
=
&
createdAt
invitees
=
append
(
invitees
,
item
)
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
invitees
,
nil
}
func
(
r
*
affiliateRepository
)
withTx
(
ctx
context
.
Context
,
fn
func
(
txCtx
context
.
Context
,
txClient
*
dbent
.
Client
)
error
)
error
{
if
tx
:=
dbent
.
TxFromContext
(
ctx
);
tx
!=
nil
{
return
fn
(
ctx
,
tx
.
Client
())
}
tx
,
err
:=
r
.
client
.
Tx
(
ctx
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"begin affiliate transaction: %w"
,
err
)
}
defer
func
()
{
_
=
tx
.
Rollback
()
}()
txCtx
:=
dbent
.
NewTxContext
(
ctx
,
tx
)
if
err
:=
fn
(
txCtx
,
tx
.
Client
());
err
!=
nil
{
return
err
}
if
err
:=
tx
.
Commit
();
err
!=
nil
{
return
fmt
.
Errorf
(
"commit affiliate transaction: %w"
,
err
)
}
return
nil
}
func
ensureUserAffiliateWithClient
(
ctx
context
.
Context
,
client
affiliateQueryExecer
,
userID
int64
)
(
*
service
.
AffiliateSummary
,
error
)
{
summary
,
err
:=
queryAffiliateByUserID
(
ctx
,
client
,
userID
)
if
err
==
nil
{
return
summary
,
nil
}
if
!
errors
.
Is
(
err
,
service
.
ErrAffiliateProfileNotFound
)
{
return
nil
,
err
}
for
i
:=
0
;
i
<
affiliateCodeMaxAttempts
;
i
++
{
code
,
codeErr
:=
generateAffiliateCode
()
if
codeErr
!=
nil
{
return
nil
,
codeErr
}
_
,
insertErr
:=
client
.
ExecContext
(
ctx
,
`
INSERT INTO user_affiliates (user_id, aff_code, created_at, updated_at)
VALUES ($1, $2, NOW(), NOW())
ON CONFLICT (user_id) DO NOTHING`
,
userID
,
code
)
if
insertErr
==
nil
{
break
}
if
isAffiliateUniqueViolation
(
insertErr
)
{
continue
}
return
nil
,
insertErr
}
return
queryAffiliateByUserID
(
ctx
,
client
,
userID
)
}
func
queryAffiliateByUserID
(
ctx
context
.
Context
,
client
affiliateQueryExecer
,
userID
int64
)
(
*
service
.
AffiliateSummary
,
error
)
{
rows
,
err
:=
client
.
QueryContext
(
ctx
,
`
SELECT user_id,
aff_code,
inviter_id,
aff_count,
aff_quota::double precision,
aff_history_quota::double precision,
created_at,
updated_at
FROM user_affiliates
WHERE user_id = $1`
,
userID
)
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
if
!
rows
.
Next
()
{
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
nil
,
service
.
ErrAffiliateProfileNotFound
}
var
out
service
.
AffiliateSummary
var
inviterID
sql
.
NullInt64
if
err
:=
rows
.
Scan
(
&
out
.
UserID
,
&
out
.
AffCode
,
&
inviterID
,
&
out
.
AffCount
,
&
out
.
AffQuota
,
&
out
.
AffHistoryQuota
,
&
out
.
CreatedAt
,
&
out
.
UpdatedAt
,
);
err
!=
nil
{
return
nil
,
err
}
if
inviterID
.
Valid
{
out
.
InviterID
=
&
inviterID
.
Int64
}
return
&
out
,
nil
}
func
queryAffiliateByCode
(
ctx
context
.
Context
,
client
affiliateQueryExecer
,
code
string
)
(
*
service
.
AffiliateSummary
,
error
)
{
rows
,
err
:=
client
.
QueryContext
(
ctx
,
`
SELECT user_id,
aff_code,
inviter_id,
aff_count,
aff_quota::double precision,
aff_history_quota::double precision,
created_at,
updated_at
FROM user_affiliates
WHERE aff_code = $1
LIMIT 1`
,
strings
.
ToUpper
(
strings
.
TrimSpace
(
code
)))
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
if
!
rows
.
Next
()
{
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
nil
,
service
.
ErrAffiliateProfileNotFound
}
var
out
service
.
AffiliateSummary
var
inviterID
sql
.
NullInt64
if
err
:=
rows
.
Scan
(
&
out
.
UserID
,
&
out
.
AffCode
,
&
inviterID
,
&
out
.
AffCount
,
&
out
.
AffQuota
,
&
out
.
AffHistoryQuota
,
&
out
.
CreatedAt
,
&
out
.
UpdatedAt
,
);
err
!=
nil
{
return
nil
,
err
}
if
inviterID
.
Valid
{
out
.
InviterID
=
&
inviterID
.
Int64
}
return
&
out
,
nil
}
func
queryUserBalance
(
ctx
context
.
Context
,
client
affiliateQueryExecer
,
userID
int64
)
(
float64
,
error
)
{
rows
,
err
:=
client
.
QueryContext
(
ctx
,
"SELECT balance::double precision FROM users WHERE id = $1 LIMIT 1"
,
userID
,
)
if
err
!=
nil
{
return
0
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
if
!
rows
.
Next
()
{
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
0
,
err
}
return
0
,
service
.
ErrUserNotFound
}
var
balance
float64
if
err
:=
rows
.
Scan
(
&
balance
);
err
!=
nil
{
return
0
,
err
}
return
balance
,
nil
}
func
generateAffiliateCode
()
(
string
,
error
)
{
buf
:=
make
([]
byte
,
affiliateCodeLength
)
if
_
,
err
:=
rand
.
Read
(
buf
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"generate affiliate code: %w"
,
err
)
}
for
i
:=
range
buf
{
buf
[
i
]
=
affiliateCodeCharset
[
int
(
buf
[
i
])
%
len
(
affiliateCodeCharset
)]
}
return
string
(
buf
),
nil
}
func
isAffiliateUniqueViolation
(
err
error
)
bool
{
var
pqErr
*
pq
.
Error
if
errors
.
As
(
err
,
&
pqErr
)
{
return
string
(
pqErr
.
Code
)
==
"23505"
}
return
false
}
backend/internal/repository/affiliate_repo_integration_test.go
0 → 100644
View file @
f03de00c
//go:build integration
package
repository
import
(
"context"
"fmt"
"testing"
"time"
dbent
"github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/require"
)
func
querySingleFloat
(
t
*
testing
.
T
,
ctx
context
.
Context
,
client
*
dbent
.
Client
,
query
string
,
args
...
any
)
float64
{
t
.
Helper
()
rows
,
err
:=
client
.
QueryContext
(
ctx
,
query
,
args
...
)
require
.
NoError
(
t
,
err
)
defer
func
()
{
_
=
rows
.
Close
()
}()
require
.
True
(
t
,
rows
.
Next
(),
"expected one row"
)
var
value
float64
require
.
NoError
(
t
,
rows
.
Scan
(
&
value
))
require
.
NoError
(
t
,
rows
.
Err
())
return
value
}
func
querySingleInt
(
t
*
testing
.
T
,
ctx
context
.
Context
,
client
*
dbent
.
Client
,
query
string
,
args
...
any
)
int
{
t
.
Helper
()
rows
,
err
:=
client
.
QueryContext
(
ctx
,
query
,
args
...
)
require
.
NoError
(
t
,
err
)
defer
func
()
{
_
=
rows
.
Close
()
}()
require
.
True
(
t
,
rows
.
Next
(),
"expected one row"
)
var
value
int
require
.
NoError
(
t
,
rows
.
Scan
(
&
value
))
require
.
NoError
(
t
,
rows
.
Err
())
return
value
}
func
TestAffiliateRepository_TransferQuotaToBalance_UsesClaimedQuotaBeforeClear
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
tx
:=
testEntTx
(
t
)
txCtx
:=
dbent
.
NewTxContext
(
ctx
,
tx
)
client
:=
tx
.
Client
()
repo
:=
NewAffiliateRepository
(
client
,
integrationDB
)
u
:=
mustCreateUser
(
t
,
client
,
&
service
.
User
{
Email
:
fmt
.
Sprintf
(
"affiliate-transfer-%d@example.com"
,
time
.
Now
()
.
UnixNano
()),
PasswordHash
:
"hash"
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Balance
:
5.5
,
Concurrency
:
5
,
})
affCode
:=
fmt
.
Sprintf
(
"AFF%09d"
,
time
.
Now
()
.
UnixNano
()
%
1
_000_000_000
)
_
,
err
:=
client
.
ExecContext
(
txCtx
,
`
INSERT INTO user_affiliates (user_id, aff_code, aff_quota, aff_history_quota, created_at, updated_at)
VALUES ($1, $2, $3, $3, NOW(), NOW())`
,
u
.
ID
,
affCode
,
12.34
)
require
.
NoError
(
t
,
err
)
transferred
,
balance
,
err
:=
repo
.
TransferQuotaToBalance
(
txCtx
,
u
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
InDelta
(
t
,
12.34
,
transferred
,
1e-9
)
require
.
InDelta
(
t
,
17.84
,
balance
,
1e-9
)
affQuota
:=
querySingleFloat
(
t
,
txCtx
,
client
,
"SELECT aff_quota::double precision FROM user_affiliates WHERE user_id = $1"
,
u
.
ID
)
require
.
InDelta
(
t
,
0.0
,
affQuota
,
1e-9
)
persistedBalance
:=
querySingleFloat
(
t
,
txCtx
,
client
,
"SELECT balance::double precision FROM users WHERE id = $1"
,
u
.
ID
)
require
.
InDelta
(
t
,
17.84
,
persistedBalance
,
1e-9
)
ledgerCount
:=
querySingleInt
(
t
,
txCtx
,
client
,
"SELECT COUNT(*) FROM user_affiliate_ledger WHERE user_id = $1 AND action = 'transfer'"
,
u
.
ID
)
require
.
Equal
(
t
,
1
,
ledgerCount
)
}
func
TestAffiliateRepository_TransferQuotaToBalance_EmptyQuota
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
tx
:=
testEntTx
(
t
)
txCtx
:=
dbent
.
NewTxContext
(
ctx
,
tx
)
client
:=
tx
.
Client
()
repo
:=
NewAffiliateRepository
(
client
,
integrationDB
)
u
:=
mustCreateUser
(
t
,
client
,
&
service
.
User
{
Email
:
fmt
.
Sprintf
(
"affiliate-empty-%d@example.com"
,
time
.
Now
()
.
UnixNano
()),
PasswordHash
:
"hash"
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Balance
:
3.21
,
Concurrency
:
5
,
})
affCode
:=
fmt
.
Sprintf
(
"AFF%09d"
,
time
.
Now
()
.
UnixNano
()
%
1
_000_000_000
)
_
,
err
:=
client
.
ExecContext
(
txCtx
,
`
INSERT INTO user_affiliates (user_id, aff_code, aff_quota, aff_history_quota, created_at, updated_at)
VALUES ($1, $2, 0, 0, NOW(), NOW())`
,
u
.
ID
,
affCode
)
require
.
NoError
(
t
,
err
)
transferred
,
balance
,
err
:=
repo
.
TransferQuotaToBalance
(
txCtx
,
u
.
ID
)
require
.
ErrorIs
(
t
,
err
,
service
.
ErrAffiliateQuotaEmpty
)
require
.
InDelta
(
t
,
0.0
,
transferred
,
1e-9
)
require
.
InDelta
(
t
,
0.0
,
balance
,
1e-9
)
persistedBalance
:=
querySingleFloat
(
t
,
txCtx
,
client
,
"SELECT balance::double precision FROM users WHERE id = $1"
,
u
.
ID
)
require
.
InDelta
(
t
,
3.21
,
persistedBalance
,
1e-9
)
}
backend/internal/repository/wire.go
View file @
f03de00c
...
@@ -91,6 +91,7 @@ var ProviderSet = wire.NewSet(
...
@@ -91,6 +91,7 @@ var ProviderSet = wire.NewSet(
NewChannelRepository
,
NewChannelRepository
,
NewChannelMonitorRepository
,
NewChannelMonitorRepository
,
NewChannelMonitorRequestTemplateRepository
,
NewChannelMonitorRequestTemplateRepository
,
NewAffiliateRepository
,
// Cache implementations
// Cache implementations
NewGatewayCache
,
NewGatewayCache
,
...
...
backend/internal/server/api_contract_test.go
View file @
f03de00c
...
@@ -715,6 +715,7 @@ func TestAPIContracts(t *testing.T) {
...
@@ -715,6 +715,7 @@ func TestAPIContracts(t *testing.T) {
"force_email_on_third_party_signup": false,
"force_email_on_third_party_signup": false,
"default_concurrency": 5,
"default_concurrency": 5,
"default_balance": 1.25,
"default_balance": 1.25,
"affiliate_rebate_rate": 20,
"default_user_rpm_limit": 0,
"default_user_rpm_limit": 0,
"default_subscriptions": [],
"default_subscriptions": [],
"enable_model_fallback": false,
"enable_model_fallback": false,
...
@@ -895,6 +896,7 @@ func TestAPIContracts(t *testing.T) {
...
@@ -895,6 +896,7 @@ func TestAPIContracts(t *testing.T) {
"custom_endpoints": [],
"custom_endpoints": [],
"default_concurrency": 0,
"default_concurrency": 0,
"default_balance": 0,
"default_balance": 0,
"affiliate_rebate_rate": 20,
"default_user_rpm_limit": 0,
"default_user_rpm_limit": 0,
"default_subscriptions": [],
"default_subscriptions": [],
"enable_model_fallback": false,
"enable_model_fallback": false,
...
...
backend/internal/server/routes/user.go
View file @
f03de00c
...
@@ -25,6 +25,8 @@ func RegisterUserRoutes(
...
@@ -25,6 +25,8 @@ func RegisterUserRoutes(
user
.
GET
(
"/profile"
,
h
.
User
.
GetProfile
)
user
.
GET
(
"/profile"
,
h
.
User
.
GetProfile
)
user
.
PUT
(
"/password"
,
h
.
User
.
ChangePassword
)
user
.
PUT
(
"/password"
,
h
.
User
.
ChangePassword
)
user
.
PUT
(
""
,
h
.
User
.
UpdateProfile
)
user
.
PUT
(
""
,
h
.
User
.
UpdateProfile
)
user
.
GET
(
"/aff"
,
h
.
User
.
GetAffiliate
)
user
.
POST
(
"/aff/transfer"
,
h
.
User
.
TransferAffiliateQuota
)
user
.
POST
(
"/account-bindings/email/send-code"
,
h
.
User
.
SendEmailBindingCode
)
user
.
POST
(
"/account-bindings/email/send-code"
,
h
.
User
.
SendEmailBindingCode
)
user
.
POST
(
"/account-bindings/email"
,
h
.
User
.
BindEmailIdentity
)
user
.
POST
(
"/account-bindings/email"
,
h
.
User
.
BindEmailIdentity
)
user
.
DELETE
(
"/account-bindings/:provider"
,
h
.
User
.
UnbindIdentity
)
user
.
DELETE
(
"/account-bindings/:provider"
,
h
.
User
.
UnbindIdentity
)
...
...
backend/internal/service/affiliate_service.go
0 → 100644
View file @
f03de00c
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
)
}()
}
}
backend/internal/service/affiliate_service_test.go
0 → 100644
View file @
f03de00c
//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
(
""
))
}
backend/internal/service/auth_service.go
View file @
f03de00c
...
@@ -72,6 +72,7 @@ type AuthService struct {
...
@@ -72,6 +72,7 @@ type AuthService struct {
turnstileService
*
TurnstileService
turnstileService
*
TurnstileService
emailQueueService
*
EmailQueueService
emailQueueService
*
EmailQueueService
promoService
*
PromoService
promoService
*
PromoService
affiliateService
*
AffiliateService
defaultSubAssigner
DefaultSubscriptionAssigner
defaultSubAssigner
DefaultSubscriptionAssigner
}
}
...
@@ -121,13 +122,26 @@ func (s *AuthService) EntClient() *dbent.Client {
...
@@ -121,13 +122,26 @@ func (s *AuthService) EntClient() *dbent.Client {
return
s
.
entClient
return
s
.
entClient
}
}
func
(
s
*
AuthService
)
SetAffiliateService
(
affiliateService
*
AffiliateService
)
{
if
s
==
nil
{
return
}
s
.
affiliateService
=
affiliateService
}
// Register 用户注册,返回token和用户
// Register 用户注册,返回token和用户
func
(
s
*
AuthService
)
Register
(
ctx
context
.
Context
,
email
,
password
string
)
(
string
,
*
User
,
error
)
{
func
(
s
*
AuthService
)
Register
(
ctx
context
.
Context
,
email
,
password
string
)
(
string
,
*
User
,
error
)
{
return
s
.
RegisterWithVerification
(
ctx
,
email
,
password
,
""
,
""
,
""
)
return
s
.
RegisterWithVerification
(
ctx
,
email
,
password
,
""
,
""
,
""
)
}
}
// RegisterWithVerification 用户注册(支持邮件验证、优惠码和邀请码),返回token和用户
// RegisterWithVerification 用户注册(支持邮件验证、优惠码、邀请码和邀请返利码),返回token和用户。
func
(
s
*
AuthService
)
RegisterWithVerification
(
ctx
context
.
Context
,
email
,
password
,
verifyCode
,
promoCode
,
invitationCode
string
)
(
string
,
*
User
,
error
)
{
// 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 未配置时不允许注册)
// 检查是否开放注册(默认关闭:settingService 未配置时不允许注册)
if
s
.
settingService
==
nil
||
!
s
.
settingService
.
IsRegistrationEnabled
(
ctx
)
{
if
s
.
settingService
==
nil
||
!
s
.
settingService
.
IsRegistrationEnabled
(
ctx
)
{
return
""
,
nil
,
ErrRegDisabled
return
""
,
nil
,
ErrRegDisabled
...
@@ -223,6 +237,17 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
...
@@ -223,6 +237,17 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
}
}
s
.
postAuthUserBootstrap
(
ctx
,
user
,
"email"
,
true
)
s
.
postAuthUserBootstrap
(
ctx
,
user
,
"email"
,
true
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
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
{
if
invitationRedeemCode
!=
nil
{
...
...
backend/internal/service/domain_constants.go
View file @
f03de00c
...
@@ -18,6 +18,13 @@ const (
...
@@ -18,6 +18,13 @@ const (
RoleUser
=
domain
.
RoleUser
RoleUser
=
domain
.
RoleUser
)
)
// Affiliate rebate settings
const
(
AffiliateRebateRateDefault
=
20.0
AffiliateRebateRateMin
=
0.0
AffiliateRebateRateMax
=
100.0
)
// Platform constants
// Platform constants
const
(
const
(
PlatformAnthropic
=
domain
.
PlatformAnthropic
PlatformAnthropic
=
domain
.
PlatformAnthropic
...
@@ -87,6 +94,7 @@ const (
...
@@ -87,6 +94,7 @@ const (
SettingKeyPasswordResetEnabled
=
"password_reset_enabled"
// 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyPasswordResetEnabled
=
"password_reset_enabled"
// 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyFrontendURL
=
"frontend_url"
// 前端基础URL,用于生成邮件中的重置密码链接
SettingKeyFrontendURL
=
"frontend_url"
// 前端基础URL,用于生成邮件中的重置密码链接
SettingKeyInvitationCodeEnabled
=
"invitation_code_enabled"
// 是否启用邀请码注册
SettingKeyInvitationCodeEnabled
=
"invitation_code_enabled"
// 是否启用邀请码注册
SettingKeyAffiliateRebateRate
=
"affiliate_rebate_rate"
// 邀请返利比例(百分比,0-100)
// 邮件服务设置
// 邮件服务设置
SettingKeySMTPHost
=
"smtp_host"
// SMTP服务器地址
SettingKeySMTPHost
=
"smtp_host"
// SMTP服务器地址
...
...
backend/internal/service/payment_fulfillment.go
View file @
f03de00c
...
@@ -2,6 +2,7 @@ package service
...
@@ -2,6 +2,7 @@ package service
import
(
import
(
"context"
"context"
"encoding/json"
"errors"
"errors"
"fmt"
"fmt"
"log/slog"
"log/slog"
...
@@ -268,6 +269,7 @@ func (s *PaymentService) doBalance(ctx context.Context, o *dbent.PaymentOrder) e
...
@@ -268,6 +269,7 @@ func (s *PaymentService) doBalance(ctx context.Context, o *dbent.PaymentOrder) e
switch
action
{
switch
action
{
case
redeemActionSkipCompleted
:
case
redeemActionSkipCompleted
:
s
.
applyAffiliateRebateForOrder
(
ctx
,
o
)
// Code already created and redeemed — just mark completed
// Code already created and redeemed — just mark completed
return
s
.
markCompleted
(
ctx
,
o
,
"RECHARGE_SUCCESS"
)
return
s
.
markCompleted
(
ctx
,
o
,
"RECHARGE_SUCCESS"
)
case
redeemActionCreate
:
case
redeemActionCreate
:
...
@@ -281,6 +283,7 @@ func (s *PaymentService) doBalance(ctx context.Context, o *dbent.PaymentOrder) e
...
@@ -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
{
if
_
,
err
:=
s
.
redeemService
.
Redeem
(
ctx
,
o
.
UserID
,
o
.
RechargeCode
);
err
!=
nil
{
return
fmt
.
Errorf
(
"redeem balance: %w"
,
err
)
return
fmt
.
Errorf
(
"redeem balance: %w"
,
err
)
}
}
s
.
applyAffiliateRebateForOrder
(
ctx
,
o
)
return
s
.
markCompleted
(
ctx
,
o
,
"RECHARGE_SUCCESS"
)
return
s
.
markCompleted
(
ctx
,
o
,
"RECHARGE_SUCCESS"
)
}
}
...
@@ -358,6 +361,139 @@ func (s *PaymentService) hasAuditLog(ctx context.Context, orderID int64, action
...
@@ -358,6 +361,139 @@ func (s *PaymentService) hasAuditLog(ctx context.Context, orderID int64, action
return
c
>
0
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
)
{
func
(
s
*
PaymentService
)
markFailed
(
ctx
context
.
Context
,
oid
int64
,
cause
error
)
{
now
:=
time
.
Now
()
now
:=
time
.
Now
()
r
:=
psErrMsg
(
cause
)
r
:=
psErrMsg
(
cause
)
...
...
backend/internal/service/payment_service.go
View file @
f03de00c
...
@@ -181,6 +181,7 @@ type PaymentService struct {
...
@@ -181,6 +181,7 @@ type PaymentService struct {
userRepo
UserRepository
userRepo
UserRepository
groupRepo
GroupRepository
groupRepo
GroupRepository
resumeService
*
PaymentResumeService
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
{
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
...
@@ -189,6 +190,13 @@ func NewPaymentService(entClient *dbent.Client, registry *payment.Registry, load
return
svc
return
svc
}
}
func
(
s
*
PaymentService
)
SetAffiliateService
(
affiliateService
*
AffiliateService
)
{
if
s
==
nil
{
return
}
s
.
affiliateService
=
affiliateService
}
// --- Provider Registry ---
// --- Provider Registry ---
// EnsureProviders lazily initializes the provider registry on first call.
// EnsureProviders lazily initializes the provider registry on first call.
...
...
backend/internal/service/setting_service.go
View file @
f03de00c
...
@@ -8,6 +8,7 @@ import (
...
@@ -8,6 +8,7 @@ import (
"errors"
"errors"
"fmt"
"fmt"
"log/slog"
"log/slog"
"math"
"net/url"
"net/url"
"sort"
"sort"
"strconv"
"strconv"
...
@@ -1167,6 +1168,8 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
...
@@ -1167,6 +1168,8 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
// 默认配置
// 默认配置
updates
[
SettingKeyDefaultConcurrency
]
=
strconv
.
Itoa
(
settings
.
DefaultConcurrency
)
updates
[
SettingKeyDefaultConcurrency
]
=
strconv
.
Itoa
(
settings
.
DefaultConcurrency
)
updates
[
SettingKeyDefaultBalance
]
=
strconv
.
FormatFloat
(
settings
.
DefaultBalance
,
'f'
,
8
,
64
)
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
)
updates
[
SettingKeyDefaultUserRPMLimit
]
=
strconv
.
Itoa
(
settings
.
DefaultUserRPMLimit
)
defaultSubsJSON
,
err
:=
json
.
Marshal
(
settings
.
DefaultSubscriptions
)
defaultSubsJSON
,
err
:=
json
.
Marshal
(
settings
.
DefaultSubscriptions
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -1719,6 +1722,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
...
@@ -1719,6 +1722,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyOIDCConnectUserInfoUsernamePath
:
""
,
SettingKeyOIDCConnectUserInfoUsernamePath
:
""
,
SettingKeyDefaultConcurrency
:
strconv
.
Itoa
(
s
.
cfg
.
Default
.
UserConcurrency
),
SettingKeyDefaultConcurrency
:
strconv
.
Itoa
(
s
.
cfg
.
Default
.
UserConcurrency
),
SettingKeyDefaultBalance
:
strconv
.
FormatFloat
(
s
.
cfg
.
Default
.
UserBalance
,
'f'
,
8
,
64
),
SettingKeyDefaultBalance
:
strconv
.
FormatFloat
(
s
.
cfg
.
Default
.
UserBalance
,
'f'
,
8
,
64
),
SettingKeyAffiliateRebateRate
:
strconv
.
FormatFloat
(
AffiliateRebateRateDefault
,
'f'
,
8
,
64
),
SettingKeyDefaultUserRPMLimit
:
"0"
,
SettingKeyDefaultUserRPMLimit
:
"0"
,
SettingKeyDefaultSubscriptions
:
"[]"
,
SettingKeyDefaultSubscriptions
:
"[]"
,
SettingKeyAuthSourceDefaultEmailBalance
:
"0"
,
SettingKeyAuthSourceDefaultEmailBalance
:
"0"
,
...
@@ -1846,6 +1850,11 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
...
@@ -1846,6 +1850,11 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
}
else
{
}
else
{
result
.
DefaultBalance
=
s
.
cfg
.
Default
.
UserBalance
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
])
result
.
DefaultSubscriptions
=
parseDefaultSubscriptions
(
settings
[
SettingKeyDefaultSubscriptions
])
// 敏感信息直接返回,方便测试连接时使用
// 敏感信息直接返回,方便测试连接时使用
...
@@ -2130,6 +2139,19 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
...
@@ -2130,6 +2139,19 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
return
result
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
{
func
isFalseSettingValue
(
value
string
)
bool
{
switch
strings
.
ToLower
(
strings
.
TrimSpace
(
value
))
{
switch
strings
.
ToLower
(
strings
.
TrimSpace
(
value
))
{
case
"false"
,
"0"
,
"off"
,
"disabled"
:
case
"false"
,
"0"
,
"off"
,
"disabled"
:
...
...
backend/internal/service/settings_view.go
View file @
f03de00c
...
@@ -106,6 +106,7 @@ type SystemSettings struct {
...
@@ -106,6 +106,7 @@ type SystemSettings struct {
DefaultConcurrency
int
DefaultConcurrency
int
DefaultBalance
float64
DefaultBalance
float64
AffiliateRebateRate
float64
DefaultUserRPMLimit
int
DefaultUserRPMLimit
int
DefaultSubscriptions
[]
DefaultSubscriptionSetting
DefaultSubscriptions
[]
DefaultSubscriptionSetting
...
...
backend/internal/service/wire.go
View file @
f03de00c
...
@@ -391,6 +391,53 @@ func ProvideSettingService(settingRepo SettingRepository, groupRepo GroupReposit
...
@@ -391,6 +391,53 @@ func ProvideSettingService(settingRepo SettingRepository, groupRepo GroupReposit
return
svc
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.
// ProvideBillingCacheService wires BillingCacheService with its RPM dependencies.
func
ProvideBillingCacheService
(
func
ProvideBillingCacheService
(
cache
BillingCache
,
cache
BillingCache
,
...
@@ -407,7 +454,7 @@ func ProvideBillingCacheService(
...
@@ -407,7 +454,7 @@ func ProvideBillingCacheService(
// ProviderSet is the Wire provider set for all services
// ProviderSet is the Wire provider set for all services
var
ProviderSet
=
wire
.
NewSet
(
var
ProviderSet
=
wire
.
NewSet
(
// Core services
// Core services
New
AuthService
,
Provide
AuthService
,
NewUserService
,
NewUserService
,
NewAPIKeyService
,
NewAPIKeyService
,
ProvideAPIKeyAuthCacheInvalidator
,
ProvideAPIKeyAuthCacheInvalidator
,
...
@@ -486,8 +533,9 @@ var ProviderSet = wire.NewSet(
...
@@ -486,8 +533,9 @@ var ProviderSet = wire.NewSet(
NewGroupCapacityService
,
NewGroupCapacityService
,
NewChannelService
,
NewChannelService
,
NewModelPricingResolver
,
NewModelPricingResolver
,
NewAffiliateService
,
ProvidePaymentConfigService
,
ProvidePaymentConfigService
,
New
PaymentService
,
Provide
PaymentService
,
ProvidePaymentOrderExpiryService
,
ProvidePaymentOrderExpiryService
,
ProvideBalanceNotifyService
,
ProvideBalanceNotifyService
,
ProvideChannelMonitorService
,
ProvideChannelMonitorService
,
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment