657 lines
20 KiB
Go
657 lines
20 KiB
Go
package rentergrowth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"hfb_sys/backend/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const (
|
|
ConfigKey = "renter.growth_level_rules"
|
|
DefaultLevelCode = "normal"
|
|
DefaultLevelName = "普通"
|
|
DefaultDiscountBps = 10000
|
|
DefaultPointsPerYuan = 1
|
|
DefaultMultiplierBps = 10_000
|
|
MaxPointsMultiplierBps = 100_000
|
|
DefaultPlatinumQuotaCent = 50_000
|
|
DefaultDiamondQuotaCent = 100_000
|
|
DefaultPeakQuotaCent = 150_000
|
|
MaxPointsPerYuan = 1_000
|
|
MaxLevelPoints = 1_000_000_000
|
|
MaxLevelQuotaCent = 100_000_000
|
|
SourceOrderCompleted = "order_completed"
|
|
SourceOrderRefunded = "order_refunded"
|
|
SourceAdminAdjustment = "admin_adjustment"
|
|
)
|
|
|
|
var fixedLevels = []struct {
|
|
Code string
|
|
Name string
|
|
}{
|
|
{Code: DefaultLevelCode, Name: DefaultLevelName},
|
|
{Code: "platinum", Name: "铂金"},
|
|
{Code: "diamond", Name: "钻石"},
|
|
{Code: "peak", Name: "巅峰"},
|
|
}
|
|
|
|
type Config struct {
|
|
Enabled bool `json:"enabled"`
|
|
PointsPerYuan int64 `json:"points_per_yuan"`
|
|
Levels []LevelRule `json:"levels"`
|
|
}
|
|
|
|
type LevelRule struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
MinPoints int64 `json:"min_points"`
|
|
PointsMultiplierBps int `json:"points_multiplier_bps"`
|
|
DiscountBps int `json:"discount_bps"`
|
|
DepositFreeQuotaCent int64 `json:"deposit_free_quota_cent"`
|
|
}
|
|
|
|
type DiscountSnapshot struct {
|
|
LevelCode string
|
|
LevelName string
|
|
DiscountBps int
|
|
UserPoints int64
|
|
LevelDepositFreeQuotaCent int64
|
|
ManualDepositFreeQuotaCent int64
|
|
EffectiveDepositFreeQuotaCent int64
|
|
}
|
|
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
Enabled: true,
|
|
PointsPerYuan: DefaultPointsPerYuan,
|
|
Levels: []LevelRule{
|
|
{Code: DefaultLevelCode, Name: DefaultLevelName, MinPoints: 0, PointsMultiplierBps: 10_000, DiscountBps: DefaultDiscountBps, DepositFreeQuotaCent: 0},
|
|
{Code: "platinum", Name: "铂金", MinPoints: 300, PointsMultiplierBps: 12_000, DiscountBps: 9900, DepositFreeQuotaCent: DefaultPlatinumQuotaCent},
|
|
{Code: "diamond", Name: "钻石", MinPoints: 1000, PointsMultiplierBps: 15_000, DiscountBps: 9800, DepositFreeQuotaCent: DefaultDiamondQuotaCent},
|
|
{Code: "peak", Name: "巅峰", MinPoints: 5000, PointsMultiplierBps: 20_000, DiscountBps: 9500, DepositFreeQuotaCent: DefaultPeakQuotaCent},
|
|
},
|
|
}
|
|
}
|
|
|
|
// UnmarshalJSON 为旧配置补齐等级免押默认值,同时保留后台显式配置的 0 元额度。
|
|
func (level *LevelRule) UnmarshalJSON(data []byte) error {
|
|
type rawLevelRule struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
MinPoints int64 `json:"min_points"`
|
|
PointsMultiplierBps *int `json:"points_multiplier_bps"`
|
|
DiscountBps int `json:"discount_bps"`
|
|
DepositFreeQuotaCent *int64 `json:"deposit_free_quota_cent"`
|
|
}
|
|
var raw rawLevelRule
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return err
|
|
}
|
|
quotaCent := defaultDepositFreeQuotaCent(raw.Code)
|
|
if raw.DepositFreeQuotaCent != nil {
|
|
quotaCent = *raw.DepositFreeQuotaCent
|
|
}
|
|
multiplierBps := defaultPointsMultiplierBps(raw.Code)
|
|
if raw.PointsMultiplierBps != nil {
|
|
multiplierBps = *raw.PointsMultiplierBps
|
|
}
|
|
*level = LevelRule{
|
|
Code: raw.Code,
|
|
Name: raw.Name,
|
|
MinPoints: raw.MinPoints,
|
|
PointsMultiplierBps: multiplierBps,
|
|
DiscountBps: raw.DiscountBps,
|
|
DepositFreeQuotaCent: quotaCent,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DefaultConfigValue() string {
|
|
raw, err := json.Marshal(DefaultConfig())
|
|
if err != nil {
|
|
return `{"enabled":true,"points_per_yuan":1,"levels":[{"code":"normal","name":"普通","min_points":0,"discount_bps":10000}]}`
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
// ParseConfigValue 严格解析后台提交的成长等级配置。
|
|
func ParseConfigValue(value string) (Config, error) {
|
|
var cfg Config
|
|
if err := json.Unmarshal([]byte(value), &cfg); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := ValidateConfig(cfg); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return normalizeConfig(cfg), nil
|
|
}
|
|
|
|
func MarshalConfig(cfg Config) (string, error) {
|
|
raw, err := json.Marshal(normalizeConfig(cfg))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(raw), nil
|
|
}
|
|
|
|
// ValidateConfig 保证四个固定等级完整,且权益不会随等级提升而倒退。
|
|
func ValidateConfig(cfg Config) error {
|
|
if cfg.PointsPerYuan <= 0 || cfg.PointsPerYuan > MaxPointsPerYuan || len(cfg.Levels) != len(fixedLevels) {
|
|
return errors.New("invalid renter growth config")
|
|
}
|
|
byCode := make(map[string]LevelRule, len(cfg.Levels))
|
|
for _, level := range cfg.Levels {
|
|
level.Code = strings.TrimSpace(level.Code)
|
|
if _, exists := byCode[level.Code]; exists {
|
|
return errors.New("duplicate renter growth level")
|
|
}
|
|
byCode[level.Code] = level
|
|
}
|
|
var previous LevelRule
|
|
for index, fixed := range fixedLevels {
|
|
level, ok := byCode[fixed.Code]
|
|
if !ok || strings.TrimSpace(level.Name) != fixed.Name {
|
|
return errors.New("missing renter growth level")
|
|
}
|
|
if level.MinPoints < 0 || level.MinPoints > MaxLevelPoints || level.PointsMultiplierBps <= 0 || level.PointsMultiplierBps > MaxPointsMultiplierBps || level.DiscountBps <= 0 || level.DiscountBps > DefaultDiscountBps || level.DepositFreeQuotaCent < 0 || level.DepositFreeQuotaCent > MaxLevelQuotaCent {
|
|
return errors.New("invalid renter growth level value")
|
|
}
|
|
if index == 0 {
|
|
if level.MinPoints != 0 || level.DiscountBps != DefaultDiscountBps || level.DepositFreeQuotaCent != 0 {
|
|
return errors.New("invalid normal renter growth level")
|
|
}
|
|
previous = level
|
|
continue
|
|
}
|
|
if level.MinPoints <= previous.MinPoints || level.PointsMultiplierBps < previous.PointsMultiplierBps || level.DiscountBps > previous.DiscountBps || level.DepositFreeQuotaCent < previous.DepositFreeQuotaCent {
|
|
return errors.New("renter growth levels must increase")
|
|
}
|
|
previous = level
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ConfigForTx(tx *gorm.DB) (Config, error) {
|
|
cfg := DefaultConfig()
|
|
if tx == nil {
|
|
return cfg, nil
|
|
}
|
|
var row model.SystemConfig
|
|
if err := tx.Where("`key` = ?", ConfigKey).First(&row).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) || isMissingConfigTableError(err) {
|
|
return cfg, nil
|
|
}
|
|
return cfg, err
|
|
}
|
|
if strings.TrimSpace(row.Value) == "" {
|
|
return cfg, nil
|
|
}
|
|
if err := json.Unmarshal([]byte(row.Value), &cfg); err != nil {
|
|
return DefaultConfig(), nil
|
|
}
|
|
return normalizeConfig(cfg), nil
|
|
}
|
|
|
|
func SnapshotForUser(tx *gorm.DB, userID uint64) (DiscountSnapshot, error) {
|
|
snapshot := DiscountSnapshot{
|
|
LevelCode: DefaultLevelCode,
|
|
LevelName: DefaultLevelName,
|
|
DiscountBps: DefaultDiscountBps,
|
|
}
|
|
if tx == nil || userID == 0 {
|
|
return snapshot, nil
|
|
}
|
|
cfg, err := ConfigForTx(tx)
|
|
if err != nil {
|
|
return snapshot, err
|
|
}
|
|
var user model.User
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
|
|
return snapshot, err
|
|
}
|
|
manualQuotaCent := maxInt64(user.DepositFreeQuotaCent, 0)
|
|
snapshot.UserPoints = user.RenterGrowthPoints
|
|
snapshot.ManualDepositFreeQuotaCent = manualQuotaCent
|
|
snapshot.EffectiveDepositFreeQuotaCent = manualQuotaCent
|
|
if !cfg.Enabled {
|
|
return snapshot, nil
|
|
}
|
|
level := LevelForPoints(cfg, user.RenterGrowthPoints)
|
|
return DiscountSnapshot{
|
|
LevelCode: level.Code,
|
|
LevelName: level.Name,
|
|
DiscountBps: level.DiscountBps,
|
|
UserPoints: user.RenterGrowthPoints,
|
|
LevelDepositFreeQuotaCent: level.DepositFreeQuotaCent,
|
|
ManualDepositFreeQuotaCent: manualQuotaCent,
|
|
EffectiveDepositFreeQuotaCent: addQuotaCent(level.DepositFreeQuotaCent, manualQuotaCent),
|
|
}, nil
|
|
}
|
|
|
|
func CalculateDiscountCent(originalRentCent int64, platformFeeCent int64, discountBps int) int64 {
|
|
if originalRentCent <= 0 || platformFeeCent <= 0 {
|
|
return 0
|
|
}
|
|
if discountBps <= 0 || discountBps >= DefaultDiscountBps {
|
|
return 0
|
|
}
|
|
discountedRentCent := originalRentCent * int64(discountBps) / DefaultDiscountBps
|
|
discountCent := originalRentCent - discountedRentCent
|
|
if discountCent < 0 {
|
|
return 0
|
|
}
|
|
if discountCent > platformFeeCent {
|
|
return platformFeeCent
|
|
}
|
|
return discountCent
|
|
}
|
|
|
|
func AwardOrderCompleted(tx *gorm.DB, order *model.RentalOrder, actualPureCoinCent int64, source string) error {
|
|
return AwardOrderBothCompleted(tx, order, actualPureCoinCent, 0, source)
|
|
}
|
|
|
|
// AwardOrderBothCompleted 按租客实际支出和卖家实际收入分别发放积分。
|
|
func AwardOrderBothCompleted(tx *gorm.DB, order *model.RentalOrder, renterBasisCent int64, ownerBasisCent int64, source string) error {
|
|
if tx == nil || order == nil || order.ID == 0 || order.RenterID == 0 {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(source) == "" {
|
|
source = SourceOrderCompleted
|
|
}
|
|
cfg, err := ConfigForTx(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := awardOrderSide(tx, order, cfg, "renter", order.RenterID, renterBasisCent, source); err != nil {
|
|
return err
|
|
}
|
|
if order.OwnerID > 0 && order.OwnerID != order.RenterID {
|
|
return awardOrderSide(tx, order, cfg, "owner", order.OwnerID, ownerBasisCent, source)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func awardOrderSide(tx *gorm.DB, order *model.RentalOrder, cfg Config, role string, userID uint64, basisCent int64, source string) error {
|
|
basisCent = maxInt64(basisCent, 0)
|
|
bizKey := fmt.Sprintf("order:%d:%s:%s", order.ID, role, source)
|
|
var existing model.RenterGrowthLedger
|
|
err := tx.Where("biz_key = ?", bizKey).First(&existing).Error
|
|
if err == nil {
|
|
setOrderPointsSnapshot(order, role, existing.BasisAmountCent, existing.PointsPerYuan, existing.PointsMultiplierBps, existing.Points, existing.CreatedAt)
|
|
return nil
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
if role == "renter" {
|
|
order.GrowthPointsBasisCent = basisCent
|
|
order.GrowthPointsPerYuan = cfg.PointsPerYuan
|
|
} else {
|
|
order.OwnerGrowthPointsBasisCent = basisCent
|
|
order.OwnerGrowthPointsPerYuan = cfg.PointsPerYuan
|
|
}
|
|
if !cfg.Enabled || basisCent <= 0 {
|
|
return nil
|
|
}
|
|
var user model.User
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
|
|
return err
|
|
}
|
|
beforePoints := user.RenterGrowthPoints
|
|
beforeAvailable := user.PointsAvailable
|
|
beforeLevel := LevelForPoints(cfg, beforePoints)
|
|
multiplierBps := beforeLevel.PointsMultiplierBps
|
|
points := PointsForAmount(basisCent, cfg.PointsPerYuan, multiplierBps)
|
|
if points <= 0 {
|
|
if role == "renter" {
|
|
order.GrowthPointsMultiplierBps = multiplierBps
|
|
} else {
|
|
order.OwnerGrowthPointsMultiplierBps = multiplierBps
|
|
}
|
|
return nil
|
|
}
|
|
afterPoints := beforePoints + points
|
|
afterLevel := LevelForPoints(cfg, afterPoints)
|
|
user.RenterGrowthPoints = afterPoints
|
|
user.RenterGrowthLevel = afterLevel.Code
|
|
user.PointsAvailable = beforeAvailable + points
|
|
if err := tx.Save(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
ledger := model.RenterGrowthLedger{
|
|
UserID: userID,
|
|
Role: role,
|
|
OrderID: &order.ID,
|
|
Points: points,
|
|
BasisAmountCent: basisCent,
|
|
PointsPerYuan: cfg.PointsPerYuan,
|
|
PointsMultiplierBps: multiplierBps,
|
|
BeforePoints: beforePoints,
|
|
AfterPoints: afterPoints,
|
|
AvailableBefore: beforeAvailable,
|
|
AvailableAfter: user.PointsAvailable,
|
|
BeforeLevel: beforeLevel.Code,
|
|
AfterLevel: afterLevel.Code,
|
|
Source: source,
|
|
BizKey: &bizKey,
|
|
Remark: "订单完成发放积分",
|
|
CreatedAt: now,
|
|
}
|
|
if err := tx.Create(&ledger).Error; err != nil {
|
|
return err
|
|
}
|
|
setOrderPointsSnapshot(order, role, basisCent, cfg.PointsPerYuan, multiplierBps, points, now)
|
|
return nil
|
|
}
|
|
|
|
func setOrderPointsSnapshot(order *model.RentalOrder, role string, basisCent, perYuan int64, multiplierBps int, points int64, awardedAt time.Time) {
|
|
if role == "owner" {
|
|
order.OwnerGrowthPointsBasisCent = basisCent
|
|
order.OwnerGrowthPointsPerYuan = perYuan
|
|
order.OwnerGrowthPointsMultiplierBps = multiplierBps
|
|
order.OwnerGrowthPointsAwarded = points
|
|
if !awardedAt.IsZero() {
|
|
order.OwnerGrowthPointsAwardedAt = &awardedAt
|
|
}
|
|
return
|
|
}
|
|
order.GrowthPointsBasisCent = basisCent
|
|
order.GrowthPointsPerYuan = perYuan
|
|
order.GrowthPointsMultiplierBps = multiplierBps
|
|
order.GrowthPointsAwarded = points
|
|
if !awardedAt.IsZero() {
|
|
order.GrowthPointsAwardedAt = &awardedAt
|
|
}
|
|
}
|
|
|
|
// RevokeOrderCompleted 在已完成订单被全额人工退款后扣回该单已发放的成长积分。
|
|
func RevokeOrderCompleted(tx *gorm.DB, order *model.RentalOrder) error {
|
|
if tx == nil || order == nil || order.ID == 0 || order.RenterID == 0 {
|
|
return nil
|
|
}
|
|
cfg, err := ConfigForTx(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, side := range []struct {
|
|
role string
|
|
userID uint64
|
|
}{
|
|
{role: "renter", userID: order.RenterID},
|
|
{role: "owner", userID: order.OwnerID},
|
|
} {
|
|
if side.userID == 0 {
|
|
continue
|
|
}
|
|
if err := revokeOrderSide(tx, order, cfg, side.role, side.userID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func revokeOrderSide(tx *gorm.DB, order *model.RentalOrder, cfg Config, role string, userID uint64) error {
|
|
bizKey := fmt.Sprintf("order:%d:%s:%s", order.ID, role, SourceOrderRefunded)
|
|
var existing model.RenterGrowthLedger
|
|
err := tx.Where("biz_key = ?", bizKey).First(&existing).Error
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
var awarded model.RenterGrowthLedger
|
|
if err := tx.Where(
|
|
"order_id = ? AND role = ? AND source = ?",
|
|
order.ID,
|
|
role,
|
|
SourceOrderCompleted,
|
|
).First(&awarded).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
if role != "renter" || order.GrowthPointsAwarded <= 0 {
|
|
return nil
|
|
}
|
|
awarded = model.RenterGrowthLedger{
|
|
UserID: userID,
|
|
Role: role,
|
|
OrderID: &order.ID,
|
|
Points: order.GrowthPointsAwarded,
|
|
BasisAmountCent: order.GrowthPointsBasisCent,
|
|
PointsPerYuan: order.GrowthPointsPerYuan,
|
|
PointsMultiplierBps: order.GrowthPointsMultiplierBps,
|
|
Source: SourceOrderCompleted,
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
points := awarded.Points
|
|
if points <= 0 {
|
|
return nil
|
|
}
|
|
var user model.User
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
|
|
return err
|
|
}
|
|
beforePoints := maxInt64(user.RenterGrowthPoints, 0)
|
|
beforeAvailable := maxInt64(user.PointsAvailable, 0)
|
|
revokedPoints := minInt64(points, beforePoints)
|
|
revokedAvailable := minInt64(points, beforeAvailable)
|
|
beforeLevel := LevelForPoints(cfg, beforePoints)
|
|
afterPoints := beforePoints - revokedPoints
|
|
afterLevel := LevelForPoints(cfg, afterPoints)
|
|
if !cfg.Enabled {
|
|
beforeLevel = LevelRule{Code: DefaultLevelCode, Name: DefaultLevelName}
|
|
afterLevel = beforeLevel
|
|
}
|
|
user.RenterGrowthPoints = afterPoints
|
|
user.RenterGrowthLevel = afterLevel.Code
|
|
user.PointsAvailable = beforeAvailable - revokedAvailable
|
|
if err := tx.Save(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Create(&model.RenterGrowthLedger{
|
|
UserID: userID,
|
|
Role: role,
|
|
OrderID: &order.ID,
|
|
Points: -revokedPoints,
|
|
BasisAmountCent: awarded.BasisAmountCent,
|
|
PointsPerYuan: awarded.PointsPerYuan,
|
|
PointsMultiplierBps: awarded.PointsMultiplierBps,
|
|
BeforePoints: beforePoints,
|
|
AfterPoints: afterPoints,
|
|
AvailableBefore: beforeAvailable,
|
|
AvailableAfter: user.PointsAvailable,
|
|
BeforeLevel: beforeLevel.Code,
|
|
AfterLevel: afterLevel.Code,
|
|
Source: SourceOrderRefunded,
|
|
BizKey: &bizKey,
|
|
Remark: "已完成订单全额退款扣回积分",
|
|
CreatedAt: time.Now(),
|
|
}).Error
|
|
}
|
|
|
|
func PointsForPureCoin(actualPureCoinCent int64, pointsPerYuan int64) int64 {
|
|
return PointsForAmount(actualPureCoinCent, pointsPerYuan, DefaultMultiplierBps)
|
|
}
|
|
|
|
func PointsForAmount(basisCent int64, pointsPerYuan int64, multiplierBps int) int64 {
|
|
if basisCent <= 0 {
|
|
return 0
|
|
}
|
|
if pointsPerYuan <= 0 {
|
|
pointsPerYuan = DefaultPointsPerYuan
|
|
}
|
|
if multiplierBps <= 0 {
|
|
multiplierBps = DefaultMultiplierBps
|
|
}
|
|
return basisCent * pointsPerYuan * int64(multiplierBps) / (100 * DefaultMultiplierBps)
|
|
}
|
|
|
|
// PointsForRent 保留旧调用兼容,积分基数现统一解释为实际纯币金额。
|
|
func PointsForRent(actualPureCoinCent int64, pointsPerYuan int64) int64 {
|
|
return PointsForPureCoin(actualPureCoinCent, pointsPerYuan)
|
|
}
|
|
|
|
func LevelForPoints(cfg Config, points int64) LevelRule {
|
|
cfg = normalizeConfig(cfg)
|
|
if points < 0 {
|
|
points = 0
|
|
}
|
|
current := cfg.Levels[0]
|
|
for _, level := range cfg.Levels {
|
|
if points >= level.MinPoints {
|
|
current = level
|
|
}
|
|
}
|
|
return current
|
|
}
|
|
|
|
func EffectiveDepositFreeQuotaCent(levelQuotaCent int64, manualQuotaCent int64) int64 {
|
|
return addQuotaCent(levelQuotaCent, manualQuotaCent)
|
|
}
|
|
|
|
// SyncAllUserLevels 在等级门槛调整后同步用户表中的展示缓存,订单仍以积分实时计算为准。
|
|
func SyncAllUserLevels(tx *gorm.DB, cfg Config) error {
|
|
if tx == nil {
|
|
return nil
|
|
}
|
|
if !cfg.Enabled {
|
|
return tx.Model(&model.User{}).Where("renter_growth_level <> ?", DefaultLevelCode).Update("renter_growth_level", DefaultLevelCode).Error
|
|
}
|
|
cfg = normalizeConfig(cfg)
|
|
caseSQL := "CASE"
|
|
args := make([]any, 0, len(cfg.Levels)*2+1)
|
|
for index := len(cfg.Levels) - 1; index >= 1; index-- {
|
|
caseSQL += " WHEN renter_growth_points >= ? THEN ?"
|
|
args = append(args, cfg.Levels[index].MinPoints, cfg.Levels[index].Code)
|
|
}
|
|
caseSQL += " ELSE ? END"
|
|
args = append(args, DefaultLevelCode)
|
|
return tx.Model(&model.User{}).Where("1 = 1").Update("renter_growth_level", gorm.Expr(caseSQL, args...)).Error
|
|
}
|
|
|
|
func normalizeConfig(cfg Config) Config {
|
|
defaultCfg := DefaultConfig()
|
|
if cfg.PointsPerYuan <= 0 {
|
|
cfg.PointsPerYuan = defaultCfg.PointsPerYuan
|
|
}
|
|
if len(cfg.Levels) == 0 {
|
|
cfg.Levels = defaultCfg.Levels
|
|
}
|
|
levels := make([]LevelRule, 0, len(cfg.Levels)+1)
|
|
hasNormal := false
|
|
for _, level := range cfg.Levels {
|
|
level.Code = strings.TrimSpace(level.Code)
|
|
level.Name = strings.TrimSpace(level.Name)
|
|
if level.Code == "" {
|
|
continue
|
|
}
|
|
if level.Name == "" {
|
|
level.Name = level.Code
|
|
}
|
|
if level.MinPoints < 0 {
|
|
level.MinPoints = 0
|
|
}
|
|
if level.PointsMultiplierBps <= 0 || level.PointsMultiplierBps > MaxPointsMultiplierBps {
|
|
level.PointsMultiplierBps = defaultPointsMultiplierBps(level.Code)
|
|
}
|
|
if level.DiscountBps <= 0 || level.DiscountBps > DefaultDiscountBps {
|
|
level.DiscountBps = DefaultDiscountBps
|
|
}
|
|
if level.DepositFreeQuotaCent < 0 {
|
|
level.DepositFreeQuotaCent = 0
|
|
}
|
|
if level.Code == DefaultLevelCode {
|
|
hasNormal = true
|
|
level.Name = DefaultLevelName
|
|
level.MinPoints = 0
|
|
}
|
|
levels = append(levels, level)
|
|
}
|
|
if !hasNormal {
|
|
levels = append(levels, LevelRule{
|
|
Code: DefaultLevelCode,
|
|
Name: DefaultLevelName,
|
|
MinPoints: 0,
|
|
PointsMultiplierBps: DefaultMultiplierBps,
|
|
DiscountBps: DefaultDiscountBps,
|
|
DepositFreeQuotaCent: 0,
|
|
})
|
|
}
|
|
sort.SliceStable(levels, func(i, j int) bool {
|
|
return levels[i].MinPoints < levels[j].MinPoints
|
|
})
|
|
cfg.Levels = levels
|
|
return cfg
|
|
}
|
|
|
|
func defaultDepositFreeQuotaCent(code string) int64 {
|
|
switch strings.TrimSpace(code) {
|
|
case "platinum":
|
|
return DefaultPlatinumQuotaCent
|
|
case "diamond":
|
|
return DefaultDiamondQuotaCent
|
|
case "peak":
|
|
return DefaultPeakQuotaCent
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func defaultPointsMultiplierBps(code string) int {
|
|
switch strings.TrimSpace(code) {
|
|
case "platinum":
|
|
return 12_000
|
|
case "diamond":
|
|
return 15_000
|
|
case "peak":
|
|
return 20_000
|
|
default:
|
|
return DefaultMultiplierBps
|
|
}
|
|
}
|
|
|
|
func addQuotaCent(left int64, right int64) int64 {
|
|
left = maxInt64(left, 0)
|
|
right = maxInt64(right, 0)
|
|
const maxInt64Value = int64(^uint64(0) >> 1)
|
|
if left > maxInt64Value-right {
|
|
return maxInt64Value
|
|
}
|
|
return left + right
|
|
}
|
|
|
|
func maxInt64(left int64, right int64) int64 {
|
|
if left > right {
|
|
return left
|
|
}
|
|
return right
|
|
}
|
|
|
|
func minInt64(left int64, right int64) int64 {
|
|
if left < right {
|
|
return left
|
|
}
|
|
return right
|
|
}
|
|
|
|
func isMissingConfigTableError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
message := strings.ToLower(err.Error())
|
|
return strings.Contains(message, "no such table") ||
|
|
strings.Contains(message, "doesn't exist") ||
|
|
strings.Contains(message, "does not exist")
|
|
}
|