Implement rebate calculation, idempotent record creation, frozen release handling, and tests for rebate configuration behavior.
152 lines
3.8 KiB
Go
152 lines
3.8 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
func IsRebateEnabled() bool {
|
|
enabled, err := strconv.ParseBool(readRebateOption(model.AffRebateEnabledKey))
|
|
return err == nil && enabled
|
|
}
|
|
|
|
func GetGlobalRebateRatePercent() int {
|
|
return parseNonNegativeRebateInt(readRebateOption(model.AffRebateRatePercentKey))
|
|
}
|
|
|
|
func GetGlobalRebateFrozenDays() int {
|
|
return parseNonNegativeRebateInt(readRebateOption(model.AffRebateFrozenDaysKey))
|
|
}
|
|
|
|
func GetEffectiveRebateRate(inviter *model.User) int {
|
|
if inviter != nil && inviter.AffRebateRatePercent > 0 {
|
|
return inviter.AffRebateRatePercent
|
|
}
|
|
return GetGlobalRebateRatePercent()
|
|
}
|
|
|
|
func GetEffectiveFrozenDays(inviter *model.User) int {
|
|
if inviter != nil && inviter.AffRebateFrozenDays > 0 {
|
|
return inviter.AffRebateFrozenDays
|
|
}
|
|
return GetGlobalRebateFrozenDays()
|
|
}
|
|
|
|
func ProcessRebateAfterRecharge(invitee *model.User, orderId int, orderType string, orderAmount int) error {
|
|
if invitee == nil || invitee.InviterId <= 0 || orderAmount <= 0 {
|
|
return nil
|
|
}
|
|
if !IsRebateEnabled() {
|
|
return nil
|
|
}
|
|
if orderType == "" {
|
|
return errors.New("order type is empty")
|
|
}
|
|
if orderId <= 0 {
|
|
return errors.New("order id must be positive")
|
|
}
|
|
|
|
return model.DB.Transaction(func(tx *gorm.DB) error {
|
|
var inviter model.User
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
First(&inviter, "id = ?", invitee.InviterId).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
var existing model.RebateRecord
|
|
err := tx.Where(
|
|
"order_type = ? AND order_id = ? AND inviter_id = ? AND invitee_id = ?",
|
|
orderType, orderId, inviter.Id, invitee.Id,
|
|
).First(&existing).Error
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
|
|
rate := GetEffectiveRebateRate(&inviter)
|
|
if rate <= 0 {
|
|
return nil
|
|
}
|
|
rebateAmount := orderAmount * rate / 100
|
|
if rebateAmount <= 0 {
|
|
return nil
|
|
}
|
|
|
|
now := time.Now()
|
|
record := model.RebateRecord{
|
|
InviterId: inviter.Id,
|
|
InviteeId: invitee.Id,
|
|
OrderId: orderId,
|
|
OrderType: orderType,
|
|
OrderAmount: orderAmount,
|
|
RebateAmount: rebateAmount,
|
|
RatePercent: rate,
|
|
Status: "released",
|
|
ReleasedAt: &now,
|
|
}
|
|
if frozenDays := GetEffectiveFrozenDays(&inviter); frozenDays > 0 {
|
|
frozenUntil := now.Add(time.Duration(frozenDays) * 24 * time.Hour)
|
|
record.Status = "frozen"
|
|
record.FrozenUntil = &frozenUntil
|
|
record.ReleasedAt = nil
|
|
}
|
|
|
|
if err := tx.Create(&record).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Model(&model.User{}).Where("id = ?", inviter.Id).Updates(map[string]interface{}{
|
|
"aff_quota": gorm.Expr("aff_quota + ?", rebateAmount),
|
|
"aff_history": gorm.Expr("aff_history + ?", rebateAmount),
|
|
}).Error
|
|
})
|
|
}
|
|
|
|
func ReleaseExpiredRebates() error {
|
|
return model.DB.Transaction(func(tx *gorm.DB) error {
|
|
var records []model.RebateRecord
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("status = ? AND frozen_until IS NOT NULL AND frozen_until <= ?", "frozen", time.Now()).
|
|
Order("id ASC").
|
|
Find(&records).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
releasedAt := time.Now()
|
|
for _, record := range records {
|
|
result := tx.Model(&model.RebateRecord{}).
|
|
Where("id = ? AND status = ?", record.Id, "frozen").
|
|
Updates(map[string]interface{}{
|
|
"status": "released",
|
|
"released_at": releasedAt,
|
|
})
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func readRebateOption(key string) string {
|
|
common.OptionMapRWMutex.RLock()
|
|
defer common.OptionMapRWMutex.RUnlock()
|
|
return common.OptionMap[key]
|
|
}
|
|
|
|
func parseNonNegativeRebateInt(value string) int {
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil || parsed < 0 {
|
|
return 0
|
|
}
|
|
return parsed
|
|
}
|