完善租客等级免押与积分管理
This commit is contained in:
@@ -14,14 +14,31 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigKey = "renter.growth_level_rules"
|
||||
DefaultLevelCode = "normal"
|
||||
DefaultLevelName = "普通"
|
||||
DefaultDiscountBps = 10000
|
||||
DefaultPointsPerYuan = 1
|
||||
SourceOrderCompleted = "order_completed"
|
||||
ConfigKey = "renter.growth_level_rules"
|
||||
DefaultLevelCode = "normal"
|
||||
DefaultLevelName = "普通"
|
||||
DefaultDiscountBps = 10000
|
||||
DefaultPointsPerYuan = 1
|
||||
DefaultPlatinumQuotaCent = 50_000
|
||||
DefaultDiamondQuotaCent = 100_000
|
||||
DefaultPeakQuotaCent = 150_000
|
||||
MaxPointsPerYuan = 1_000
|
||||
MaxLevelPoints = 1_000_000_000
|
||||
MaxLevelQuotaCent = 100_000_000
|
||||
SourceOrderCompleted = "order_completed"
|
||||
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"`
|
||||
@@ -29,17 +46,21 @@ type Config struct {
|
||||
}
|
||||
|
||||
type LevelRule struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
MinPoints int64 `json:"min_points"`
|
||||
DiscountBps int `json:"discount_bps"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
MinPoints int64 `json:"min_points"`
|
||||
DiscountBps int `json:"discount_bps"`
|
||||
DepositFreeQuotaCent int64 `json:"deposit_free_quota_cent"`
|
||||
}
|
||||
|
||||
type DiscountSnapshot struct {
|
||||
LevelCode string
|
||||
LevelName string
|
||||
DiscountBps int
|
||||
UserPoints int64
|
||||
LevelCode string
|
||||
LevelName string
|
||||
DiscountBps int
|
||||
UserPoints int64
|
||||
LevelDepositFreeQuotaCent int64
|
||||
ManualDepositFreeQuotaCent int64
|
||||
EffectiveDepositFreeQuotaCent int64
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
@@ -47,14 +68,41 @@ func DefaultConfig() Config {
|
||||
Enabled: true,
|
||||
PointsPerYuan: DefaultPointsPerYuan,
|
||||
Levels: []LevelRule{
|
||||
{Code: DefaultLevelCode, Name: DefaultLevelName, MinPoints: 0, DiscountBps: DefaultDiscountBps},
|
||||
{Code: "platinum", Name: "铂金", MinPoints: 300, DiscountBps: 9900},
|
||||
{Code: "diamond", Name: "钻石", MinPoints: 1000, DiscountBps: 9800},
|
||||
{Code: "peak", Name: "巅峰", MinPoints: 5000, DiscountBps: 9500},
|
||||
{Code: DefaultLevelCode, Name: DefaultLevelName, MinPoints: 0, DiscountBps: DefaultDiscountBps, DepositFreeQuotaCent: 0},
|
||||
{Code: "platinum", Name: "铂金", MinPoints: 300, DiscountBps: 9900, DepositFreeQuotaCent: DefaultPlatinumQuotaCent},
|
||||
{Code: "diamond", Name: "钻石", MinPoints: 1000, DiscountBps: 9800, DepositFreeQuotaCent: DefaultDiamondQuotaCent},
|
||||
{Code: "peak", Name: "巅峰", MinPoints: 5000, 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"`
|
||||
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
|
||||
}
|
||||
*level = LevelRule{
|
||||
Code: raw.Code,
|
||||
Name: raw.Name,
|
||||
MinPoints: raw.MinPoints,
|
||||
DiscountBps: raw.DiscountBps,
|
||||
DepositFreeQuotaCent: quotaCent,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultConfigValue() string {
|
||||
raw, err := json.Marshal(DefaultConfig())
|
||||
if err != nil {
|
||||
@@ -63,6 +111,63 @@ func DefaultConfigValue() string {
|
||||
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.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.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 {
|
||||
@@ -97,19 +202,26 @@ func SnapshotForUser(tx *gorm.DB, userID uint64) (DiscountSnapshot, error) {
|
||||
if err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
return snapshot, nil
|
||||
}
|
||||
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,
|
||||
LevelCode: level.Code,
|
||||
LevelName: level.Name,
|
||||
DiscountBps: level.DiscountBps,
|
||||
UserPoints: user.RenterGrowthPoints,
|
||||
LevelDepositFreeQuotaCent: level.DepositFreeQuotaCent,
|
||||
ManualDepositFreeQuotaCent: manualQuotaCent,
|
||||
EffectiveDepositFreeQuotaCent: addQuotaCent(level.DepositFreeQuotaCent, manualQuotaCent),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -182,7 +294,7 @@ func AwardOrderCompleted(tx *gorm.DB, order *model.RentalOrder, actualRentCent i
|
||||
now := time.Now()
|
||||
ledger := model.RenterGrowthLedger{
|
||||
UserID: order.RenterID,
|
||||
OrderID: order.ID,
|
||||
OrderID: &order.ID,
|
||||
Points: points,
|
||||
BeforePoints: beforePoints,
|
||||
AfterPoints: afterPoints,
|
||||
@@ -224,6 +336,30 @@ func LevelForPoints(cfg Config, points int64) LevelRule {
|
||||
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 {
|
||||
@@ -249,6 +385,9 @@ func normalizeConfig(cfg Config) Config {
|
||||
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
|
||||
@@ -258,10 +397,11 @@ func normalizeConfig(cfg Config) Config {
|
||||
}
|
||||
if !hasNormal {
|
||||
levels = append(levels, LevelRule{
|
||||
Code: DefaultLevelCode,
|
||||
Name: DefaultLevelName,
|
||||
MinPoints: 0,
|
||||
DiscountBps: DefaultDiscountBps,
|
||||
Code: DefaultLevelCode,
|
||||
Name: DefaultLevelName,
|
||||
MinPoints: 0,
|
||||
DiscountBps: DefaultDiscountBps,
|
||||
DepositFreeQuotaCent: 0,
|
||||
})
|
||||
}
|
||||
sort.SliceStable(levels, func(i, j int) bool {
|
||||
@@ -271,6 +411,36 @@ func normalizeConfig(cfg Config) Config {
|
||||
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 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 isMissingConfigTableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user