Files
hfb_sys/backend/internal/modules/rentergrowth/growth.go
T

283 lines
7.0 KiB
Go

package rentergrowth
import (
"encoding/json"
"errors"
"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
SourceOrderCompleted = "order_completed"
)
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"`
DiscountBps int `json:"discount_bps"`
}
type DiscountSnapshot struct {
LevelCode string
LevelName string
DiscountBps int
UserPoints int64
}
func DefaultConfig() Config {
return 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},
},
}
}
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)
}
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
}
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
}
level := LevelForPoints(cfg, user.RenterGrowthPoints)
return DiscountSnapshot{
LevelCode: level.Code,
LevelName: level.Name,
DiscountBps: level.DiscountBps,
UserPoints: user.RenterGrowthPoints,
}, 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, actualRentCent int64, source string) error {
if tx == nil || order == nil || order.ID == 0 || order.RenterID == 0 {
return nil
}
if order.GrowthPointsAwarded > 0 || order.GrowthPointsAwardedAt != nil {
return nil
}
if strings.TrimSpace(source) == "" {
source = SourceOrderCompleted
}
var existing model.RenterGrowthLedger
err := tx.Where("order_id = ? AND source = ?", order.ID, source).First(&existing).Error
if err == nil {
order.GrowthPointsAwarded = existing.Points
if !existing.CreatedAt.IsZero() {
awardedAt := existing.CreatedAt
order.GrowthPointsAwardedAt = &awardedAt
}
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
cfg, err := ConfigForTx(tx)
if err != nil {
return err
}
if !cfg.Enabled {
return nil
}
points := PointsForRent(actualRentCent, cfg.PointsPerYuan)
if points <= 0 {
return nil
}
var user model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, order.RenterID).Error; err != nil {
return err
}
beforePoints := user.RenterGrowthPoints
beforeLevel := LevelForPoints(cfg, beforePoints)
afterPoints := beforePoints + points
afterLevel := LevelForPoints(cfg, afterPoints)
user.RenterGrowthPoints = afterPoints
user.RenterGrowthLevel = afterLevel.Code
if err := tx.Save(&user).Error; err != nil {
return err
}
now := time.Now()
ledger := model.RenterGrowthLedger{
UserID: order.RenterID,
OrderID: order.ID,
Points: points,
BeforePoints: beforePoints,
AfterPoints: afterPoints,
BeforeLevel: beforeLevel.Code,
AfterLevel: afterLevel.Code,
Source: source,
Remark: "租号订单完成发放成长积分",
CreatedAt: now,
}
if err := tx.Create(&ledger).Error; err != nil {
return err
}
order.GrowthPointsAwarded = points
order.GrowthPointsAwardedAt = &now
return nil
}
func PointsForRent(actualRentCent int64, pointsPerYuan int64) int64 {
if actualRentCent <= 0 {
return 0
}
if pointsPerYuan <= 0 {
pointsPerYuan = DefaultPointsPerYuan
}
return (actualRentCent / 100) * 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 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.DiscountBps <= 0 || level.DiscountBps > DefaultDiscountBps {
level.DiscountBps = DefaultDiscountBps
}
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,
DiscountBps: DefaultDiscountBps,
})
}
sort.SliceStable(levels, func(i, j int) bool {
return levels[i].MinPoints < levels[j].MinPoints
})
cfg.Levels = levels
return cfg
}
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")
}