feat(user): 支持后台调整等级并限制极品专区可见性

This commit is contained in:
yml2213
2026-11-05 12:02:48 +08:00
parent 09f244a93d
commit 0d558bdce0
35 changed files with 934 additions and 131 deletions
@@ -221,10 +221,10 @@ func SnapshotForUser(tx *gorm.DB, userID uint64) (DiscountSnapshot, error) {
snapshot.UserPoints = user.RenterGrowthPoints
snapshot.ManualDepositFreeQuotaCent = manualQuotaCent
snapshot.EffectiveDepositFreeQuotaCent = manualQuotaCent
if !cfg.Enabled {
if !cfg.Enabled && strings.TrimSpace(user.RenterGrowthLevelOverride) == "" {
return snapshot, nil
}
level := LevelForPoints(cfg, user.RenterGrowthPoints)
level := EffectiveLevelForUser(cfg, user.RenterGrowthPoints, user.RenterGrowthLevelOverride)
return DiscountSnapshot{
LevelCode: level.Code,
LevelName: level.Name,
@@ -307,7 +307,7 @@ func awardOrderSide(tx *gorm.DB, order *model.RentalOrder, cfg Config, role stri
}
beforePoints := user.RenterGrowthPoints
beforeAvailable := user.PointsAvailable
beforeLevel := LevelForPoints(cfg, beforePoints)
beforeLevel := EffectiveLevelForUser(cfg, beforePoints, user.RenterGrowthLevelOverride)
multiplierBps := beforeLevel.PointsMultiplierBps
points := PointsForAmount(basisCent, cfg.PointsPerYuan, multiplierBps)
if points <= 0 {
@@ -319,7 +319,7 @@ func awardOrderSide(tx *gorm.DB, order *model.RentalOrder, cfg Config, role stri
return nil
}
afterPoints := beforePoints + points
afterLevel := LevelForPoints(cfg, afterPoints)
afterLevel := LevelForUserAfterPoints(cfg, afterPoints, user.RenterGrowthLevelOverride)
user.RenterGrowthPoints = afterPoints
user.RenterGrowthLevel = afterLevel.Code
user.PointsAvailable = beforeAvailable + points
@@ -446,10 +446,10 @@ func revokeOrderSide(tx *gorm.DB, order *model.RentalOrder, cfg Config, role str
beforeAvailable := maxInt64(user.PointsAvailable, 0)
revokedPoints := minInt64(points, beforePoints)
revokedAvailable := minInt64(points, beforeAvailable)
beforeLevel := LevelForPoints(cfg, beforePoints)
beforeLevel := EffectiveLevelForUser(cfg, beforePoints, user.RenterGrowthLevelOverride)
afterPoints := beforePoints - revokedPoints
afterLevel := LevelForPoints(cfg, afterPoints)
if !cfg.Enabled {
afterLevel := LevelForUserAfterPoints(cfg, afterPoints, user.RenterGrowthLevelOverride)
if !cfg.Enabled && user.RenterGrowthLevelOverride == "" {
beforeLevel = LevelRule{Code: DefaultLevelCode, Name: DefaultLevelName}
afterLevel = beforeLevel
}
@@ -516,6 +516,43 @@ func LevelForPoints(cfg Config, points int64) LevelRule {
return current
}
func LevelForCode(cfg Config, code string) (LevelRule, bool) {
cfg = normalizeConfig(cfg)
code = strings.TrimSpace(code)
if code == "" {
return LevelRule{}, false
}
for _, level := range cfg.Levels {
if level.Code == code {
return level, true
}
}
return LevelRule{}, false
}
func EffectiveLevelForUser(cfg Config, points int64, override string) LevelRule {
if level, ok := LevelForCode(cfg, override); ok {
return level
}
return LevelForPoints(cfg, points)
}
func LevelForUserAfterPoints(cfg Config, points int64, override string) LevelRule {
if level, ok := LevelForCode(cfg, override); ok {
return level
}
return LevelForPoints(cfg, points)
}
func CanViewPremiumLevel(code string) bool {
switch strings.TrimSpace(code) {
case "platinum", "diamond", "peak":
return true
default:
return false
}
}
func EffectiveDepositFreeQuotaCent(levelQuotaCent int64, manualQuotaCent int64) int64 {
return addQuotaCent(levelQuotaCent, manualQuotaCent)
}
@@ -526,7 +563,14 @@ func SyncAllUserLevels(tx *gorm.DB, cfg Config) error {
return nil
}
if !cfg.Enabled {
return tx.Model(&model.User{}).Where("renter_growth_level <> ?", DefaultLevelCode).Update("renter_growth_level", DefaultLevelCode).Error
if err := tx.Model(&model.User{}).
Where("renter_growth_level_override = ? AND renter_growth_level <> ?", "", DefaultLevelCode).
Update("renter_growth_level", DefaultLevelCode).Error; err != nil {
return err
}
return tx.Model(&model.User{}).
Where("renter_growth_level_override <> ?", "").
UpdateColumn("renter_growth_level", gorm.Expr("renter_growth_level_override")).Error
}
cfg = normalizeConfig(cfg)
caseSQL := "CASE"
@@ -537,7 +581,14 @@ func SyncAllUserLevels(tx *gorm.DB, cfg Config) error {
}
caseSQL += " ELSE ? END"
args = append(args, DefaultLevelCode)
return tx.Model(&model.User{}).Where("1 = 1").Update("renter_growth_level", gorm.Expr(caseSQL, args...)).Error
if err := tx.Model(&model.User{}).
Where("renter_growth_level_override = ?", "").
Update("renter_growth_level", gorm.Expr(caseSQL, args...)).Error; err != nil {
return err
}
return tx.Model(&model.User{}).
Where("renter_growth_level_override <> ?", "").
UpdateColumn("renter_growth_level", gorm.Expr("renter_growth_level_override")).Error
}
func normalizeConfig(cfg Config) Config {
@@ -128,6 +128,48 @@ func TestLevelForPointsStartsFromNormal(t *testing.T) {
}
}
func TestEffectiveLevelForUserHonorsManualOverride(t *testing.T) {
cfg := DefaultConfig()
if got := EffectiveLevelForUser(cfg, 0, "diamond"); got.Code != "diamond" {
t.Fatalf("手动等级 = %s, want diamond", got.Code)
}
if got := EffectiveLevelForUser(cfg, 5000, ""); got.Code != "peak" {
t.Fatalf("自动等级 = %s, want peak", got.Code)
}
if !CanViewPremiumLevel("platinum") || !CanViewPremiumLevel("diamond") || !CanViewPremiumLevel("peak") {
t.Fatal("铂金及以上等级应可查看极品专区")
}
if CanViewPremiumLevel(DefaultLevelCode) {
t.Fatal("普通等级不应查看极品专区")
}
}
func TestSyncAllUserLevelsPreservesManualOverride(t *testing.T) {
db := database.NewTestDB()
if err := database.AutoMigrateIfMissing(db, &model.User{}); err != nil {
t.Fatalf("migrate failed: %v", err)
}
user := model.User{
Phone: "13800009003",
RenterGrowthPoints: 0,
RenterGrowthLevel: "platinum",
RenterGrowthLevelOverride: "platinum",
}
if err := db.Create(&user).Error; err != nil {
t.Fatalf("create user failed: %v", err)
}
if err := SyncAllUserLevels(db, DefaultConfig()); err != nil {
t.Fatalf("SyncAllUserLevels() error = %v", err)
}
var saved model.User
if err := db.First(&saved, user.ID).Error; err != nil {
t.Fatalf("find user failed: %v", err)
}
if saved.RenterGrowthLevel != "platinum" || saved.RenterGrowthLevelOverride != "platinum" {
t.Fatalf("手动等级被同步覆盖:%+v", saved)
}
}
func TestCalculateDiscountCentCapsAtPlatformFee(t *testing.T) {
if got := CalculateDiscountCent(10000, 60, 9900); got != 60 {
t.Fatalf("discount = %d, want capped platform fee 60", got)