Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ var TelegramBotName = ""
var QuotaForNewUser = 0
var QuotaForInviter = 0
var QuotaForInvitee = 0

// Referral commission settings (payment-based referral)
var ReferralCommissionEnabled = false // Enable commission when referred user recharges
var ReferralCommissionPercent = 10.0 // Percentage of recharge amount (0-100)
var ReferralCommissionMaxRecharges = 0 // Max recharges to give commission (0 = unlimited)
var ChannelDisableThreshold = 5.0
var AutomaticDisableChannelEnabled = false
var AutomaticEnableChannelEnabled = false
Expand Down
3 changes: 3 additions & 0 deletions controller/topup.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@ func EpayNotify(c *gin.Context) {
}
logger.LogInfo(c.Request.Context(), fmt.Sprintf("易支付 充值成功 trade_no=%s user_id=%d client_ip=%s quota_to_add=%d money=%.2f topup=%q", topUp.TradeNo, topUp.UserId, c.ClientIP(), quotaToAdd, topUp.Money, common.GetJsonString(topUp)))
model.RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money), c.ClientIP(), topUp.PaymentMethod, "epay")
if err := model.CreditReferralCommission(topUp.UserId, topUp.Money, "epay", topUp.Id); err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("返佣失败 user_id=%d topup_id=%d trade_no=%s payment_method=%s err=%v", topUp.UserId, topUp.Id, topUp.TradeNo, topUp.PaymentMethod, err))
}
}
} else {
logger.LogInfo(c.Request.Context(), fmt.Sprintf("易支付 webhook 忽略事件 trade_no=%s callback_type=%s trade_status=%s client_ip=%s verify_info=%q", verifyInfo.ServiceTradeNo, verifyInfo.Type, verifyInfo.TradeStatus, c.ClientIP(), common.GetJsonString(verifyInfo)))
Expand Down
45 changes: 40 additions & 5 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,32 @@ func GetAffCode(c *gin.Context) {
return
}

func GetInvitedUsers(c *gin.Context) {
id := c.GetInt("id")
pageInfo := common.GetPageQuery(c)
users, total, err := model.GetInvitedUsers(id, pageInfo)
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(users)
common.ApiSuccess(c, pageInfo)
}

func GetReferralCommissions(c *gin.Context) {
id := c.GetInt("id")
pageInfo := common.GetPageQuery(c)
commissions, total, err := model.GetUserReferralCommissions(id, pageInfo)
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(commissions)
common.ApiSuccess(c, pageInfo)
}

func GetSelf(c *gin.Context) {
id := c.GetInt("id")
userRole := c.GetInt("role")
Expand Down Expand Up @@ -532,11 +558,13 @@ func buildSelfUserData(user *model.User) map[string]interface{} {
"quota": user.Quota,
"used_quota": user.UsedQuota,
"request_count": user.RequestCount,
"aff_code": user.AffCode,
"aff_count": user.AffCount,
"aff_quota": user.AffQuota,
"aff_history_quota": user.AffHistoryQuota,
"inviter_id": user.InviterId,
"aff_code": user.AffCode,
"aff_count": user.AffCount,
"aff_quota": user.AffQuota,
"aff_history_quota": user.AffHistoryQuota,
"aff_commission_rate": effectiveCommissionRate(user.ReferralCommissionPercent),
"aff_commission_max_recharges": common.ReferralCommissionMaxRecharges,
"inviter_id": user.InviterId,
"linux_do_id": user.LinuxDOId,
"setting": user.Setting,
"stripe_customer": user.StripeCustomer,
Expand All @@ -545,6 +573,13 @@ func buildSelfUserData(user *model.User) map[string]interface{} {
}
}

func effectiveCommissionRate(perUser *float64) float64 {
if perUser != nil {
return *perUser
}
return common.ReferralCommissionPercent
}

// 计算用户权限的辅助函数
func calculateUserPermissions(userRole int) map[string]interface{} {
permissions := map[string]interface{}{}
Expand Down
2 changes: 1 addition & 1 deletion model/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type Channel struct {
Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
ParamOverride *string `json:"param_override" gorm:"type:text"`
HeaderOverride *string `json:"header_override" gorm:"type:text"`
Remark *string `json:"remark" gorm:"type:varchar(255)" validate:"max=255"`
Remark *string `json:"remark" gorm:"type:varchar(255)" validate:"omitempty,max=255"`
// add after v0.8.5
ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`

Expand Down
2 changes: 2 additions & 0 deletions model/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ func migrateDB() error {
&SystemTaskLock{},
&CasbinRule{},
&AuthzRole{},
&ReferralCommission{},
)
if err != nil {
return err
Expand Down Expand Up @@ -363,6 +364,7 @@ func migrateDBFast() error {
{&SystemInstance{}, "SystemInstance"},
{&SystemTask{}, "SystemTask"},
{&SystemTaskLock{}, "SystemTaskLock"},
{&ReferralCommission{}, "ReferralCommission"},
}
// 动态计算migration数量,确保errChan缓冲区足够大
errChan := make(chan error, len(migrations))
Expand Down
13 changes: 13 additions & 0 deletions model/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ func InitOptionMap() {
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
common.OptionMap["QuotaForInviter"] = strconv.Itoa(common.QuotaForInviter)
common.OptionMap["QuotaForInvitee"] = strconv.Itoa(common.QuotaForInvitee)
common.OptionMap["ReferralCommissionEnabled"] = strconv.FormatBool(common.ReferralCommissionEnabled)
common.OptionMap["ReferralCommissionPercent"] = strconv.FormatFloat(common.ReferralCommissionPercent, 'f', -1, 64)
common.OptionMap["ReferralCommissionMaxRecharges"] = strconv.Itoa(common.ReferralCommissionMaxRecharges)
common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold)
common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
Expand Down Expand Up @@ -370,6 +373,8 @@ func updateOptionMap(key string, value string) (err error) {
setting.DefaultUseAutoGroup = boolValue
case "ExposeRatioEnabled":
ratio_setting.SetExposeRatioEnabled(boolValue)
case "ReferralCommissionEnabled":
common.ReferralCommissionEnabled = boolValue
}
}
switch key {
Expand Down Expand Up @@ -512,6 +517,14 @@ func updateOptionMap(key string, value string) (err error) {
common.QuotaForInviter, _ = strconv.Atoi(value)
case "QuotaForInvitee":
common.QuotaForInvitee, _ = strconv.Atoi(value)
case "ReferralCommissionPercent":
if v, err := strconv.ParseFloat(value, 64); err == nil && v >= 0 && v <= 100 {
common.ReferralCommissionPercent = v
}
case "ReferralCommissionMaxRecharges":
if v, parseErr := strconv.Atoi(value); parseErr == nil && v >= 0 {
common.ReferralCommissionMaxRecharges = v
}
case "QuotaRemindThreshold":
common.QuotaRemindThreshold, _ = strconv.Atoi(value)
case "PreConsumedQuota":
Expand Down
72 changes: 72 additions & 0 deletions model/referral_commission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package model

import "github.com/QuantumNous/new-api/common"

type ReferralCommission struct {
Id int `json:"id" gorm:"primaryKey"`
InviterId int `json:"inviter_id" gorm:"index"`
InviteeId int `json:"invitee_id" gorm:"index;uniqueIndex:idx_invitee_topup_method"`
TopUpId int `json:"top_up_id" gorm:"uniqueIndex:idx_invitee_topup_method"`
RechargeAmount float64 `json:"recharge_amount"`
CommissionQuota int `json:"commission_quota"`
CommissionRate float64 `json:"commission_rate"`
PaymentMethod string `json:"payment_method" gorm:"type:varchar(50);uniqueIndex:idx_invitee_topup_method"`
CreatedAt int64 `json:"created_at" gorm:"autoCreateTime"`
}

type ReferralCommissionWithUser struct {
ReferralCommission
InviteeUsername string `json:"invitee_username"`
}

type InvitedUser struct {
Id int `json:"id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
Status int `json:"status"`
CommissionCount int `json:"commission_count"`
TotalEarned float64 `json:"total_earned"`
}

func GetInvitedUsers(inviterId int, pageInfo *common.PageInfo) ([]*InvitedUser, int64, error) {
var total int64
var users []*InvitedUser

countQuery := DB.Table("users").Where("inviter_id = ?", inviterId)
if err := countQuery.Count(&total).Error; err != nil {
return nil, 0, err
}

err := DB.Table("users").
Select("users.id, users.username, users.display_name, users.status, "+
"COALESCE(rc.commission_count, 0) as commission_count, "+
"COALESCE(rc.total_earned, 0) as total_earned").
Joins("LEFT JOIN (SELECT invitee_id, COUNT(*) as commission_count, SUM(commission_quota) as total_earned "+
"FROM referral_commissions WHERE inviter_id = ? GROUP BY invitee_id) rc ON rc.invitee_id = users.id", inviterId).
Where("users.inviter_id = ?", inviterId).
Order("users.id desc").
Limit(pageInfo.GetPageSize()).
Offset(pageInfo.GetStartIdx()).
Find(&users).Error
return users, total, err
}

func GetUserReferralCommissions(inviterId int, pageInfo *common.PageInfo) ([]*ReferralCommissionWithUser, int64, error) {
var total int64
var commissions []*ReferralCommissionWithUser

query := DB.Table("referral_commissions").
Select("referral_commissions.*, users.username as invitee_username").
Joins("LEFT JOIN users ON users.id = referral_commissions.invitee_id").
Where("referral_commissions.inviter_id = ?", inviterId)

if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}

err := query.Order("referral_commissions.id desc").
Limit(pageInfo.GetPageSize()).
Offset(pageInfo.GetStartIdx()).
Find(&commissions).Error
return commissions, total, err
}
8 changes: 8 additions & 0 deletions model/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ func CompleteSubscriptionOrder(tradeNo string, providerPayload string, expectedP
var logPlanTitle string
var logMoney float64
var logPaymentMethod string
var logOrderId int
var upgradeGroup string
err := DB.Transaction(func(tx *gorm.DB) error {
var order SubscriptionOrder
Expand Down Expand Up @@ -625,6 +626,7 @@ func CompleteSubscriptionOrder(tradeNo string, providerPayload string, expectedP
logPlanTitle = plan.Title
logMoney = order.Money
logPaymentMethod = order.PaymentMethod
logOrderId = order.Id
return nil
})
if err != nil {
Expand All @@ -636,6 +638,12 @@ func CompleteSubscriptionOrder(tradeNo string, providerPayload string, expectedP
if logUserId > 0 {
msg := fmt.Sprintf("订阅购买成功,套餐: %s,支付金额: %.2f,支付方式: %s", logPlanTitle, logMoney, logPaymentMethod)
RecordLog(logUserId, LogTypeTopup, msg)

// Credit referral commission to inviter (if enabled)
if err := CreditReferralCommission(logUserId, logMoney, logPaymentMethod, logOrderId); err != nil {
common.SysLog(fmt.Sprintf("返佣失败 user_id=%d topup_id=%d payment_method=%s err=%v",
logUserId, logOrderId, logPaymentMethod, err))
}
}
return nil
}
Expand Down
26 changes: 26 additions & 0 deletions model/topup.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ func Recharge(referenceId string, customerId string, callerIp string) (err error

RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(int(quota)), topUp.Amount), callerIp, topUp.PaymentMethod, PaymentMethodStripe)

// Credit referral commission to inviter (if enabled)
if err := CreditReferralCommission(topUp.UserId, topUp.Money, "stripe", topUp.Id); err != nil {
common.SysLog(fmt.Sprintf("返佣失败 user_id=%d topup_id=%d trade_no=%s payment_method=stripe err=%v",
topUp.UserId, topUp.Id, topUp.TradeNo, err))
}

return nil
}

Expand Down Expand Up @@ -331,6 +337,7 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {
var quotaToAdd int
var payMoney float64
var paymentMethod string
var topUpId int

err := DB.Transaction(func(tx *gorm.DB) error {
topUp := &TopUp{}
Expand Down Expand Up @@ -378,6 +385,7 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {
userId = topUp.UserId
payMoney = topUp.Money
paymentMethod = topUp.PaymentMethod
topUpId = topUp.Id
return nil
})

Expand All @@ -387,8 +395,14 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {

// 事务外记录日志,避免阻塞
RecordTopupLog(userId, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney), callerIp, paymentMethod, "admin")

if err := CreditReferralCommission(userId, payMoney, "manual", topUpId); err != nil {
common.SysLog(fmt.Sprintf("返佣失败 user_id=%d topup_id=%d payment_method=manual err=%v", userId, topUpId, err))
}

return nil
}

func RechargeCreem(referenceId string, customerEmail string, customerName string, callerIp string) (err error) {
if referenceId == "" {
return errors.New("未提供支付单号")
Expand Down Expand Up @@ -461,6 +475,12 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string

RecordTopupLog(topUp.UserId, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money), callerIp, topUp.PaymentMethod, PaymentMethodCreem)

// Credit referral commission to inviter (if enabled)
if err := CreditReferralCommission(topUp.UserId, topUp.Money, "creem", topUp.Id); err != nil {
common.SysLog(fmt.Sprintf("返佣失败 user_id=%d topup_id=%d trade_no=%s payment_method=creem err=%v",
topUp.UserId, topUp.Id, topUp.TradeNo, err))
}

return nil
}

Expand Down Expand Up @@ -585,5 +605,11 @@ func RechargeWaffoPancake(tradeNo string) (err error) {
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("Waffo Pancake充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money))
}

// Credit referral commission to inviter (if enabled)
if err := CreditReferralCommission(topUp.UserId, topUp.Money, "waffo", topUp.Id); err != nil {
common.SysLog(fmt.Sprintf("返佣失败 user_id=%d topup_id=%d trade_no=%s payment_method=waffo err=%v",
topUp.UserId, topUp.Id, topUp.TradeNo, err))
}

return nil
}
Loading