完善租客等级免押与积分管理

This commit is contained in:
yml2213
2026-07-26 18:07:10 +08:00
parent 379536e5c5
commit c594b543d1
24 changed files with 1296 additions and 178 deletions
+127 -51
View File
@@ -34,8 +34,12 @@ func NewRepository(db *gorm.DB, encryptors ...crypto.Encryptor) *Repository {
}
func (r *Repository) List(ctx context.Context, page, pageSize int, query ListQuery) (*PaginatedResult, error) {
growthConfig, err := rentergrowth.ConfigForTx(r.db.WithContext(ctx))
if err != nil {
return nil, err
}
var total int64
countTx := applyUserFilter(r.db.WithContext(ctx).Table("users AS u"), query)
countTx := applyUserFilter(r.db.WithContext(ctx).Table("users AS u"), query, growthConfig)
if err := countTx.Count(&total).Error; err != nil {
return nil, err
}
@@ -54,8 +58,8 @@ func (r *Repository) List(ctx context.Context, page, pageSize int, query ListQue
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id").
Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id").
Joins("LEFT JOIN wallet_accounts AS w ON w.user_id = u.id")
listTx = applyUserFilter(listTx, query)
err := listTx.
listTx = applyUserFilter(listTx, query, growthConfig)
err = listTx.
Order("u.id DESC").
Offset(offset).Limit(pageSize).
Scan(&rows).Error
@@ -64,18 +68,22 @@ func (r *Repository) List(ctx context.Context, page, pageSize int, query ListQue
}
items := make([]UserDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
items = append(items, row.toDTO(growthConfig))
}
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
// applyUserFilter 将列表筛选条件应用到查询(基于 users 表别名 u)。
func applyUserFilter(tx *gorm.DB, query ListQuery) *gorm.DB {
func applyUserFilter(tx *gorm.DB, query ListQuery, growthConfig rentergrowth.Config) *gorm.DB {
if query.Status != "" {
tx = tx.Where("u.status = ?", query.Status)
}
if query.HasDepositFreeQuota {
tx = tx.Where("u.deposit_free_quota_cent > 0")
if minPoints, ok := minimumGrowthQuotaPoints(growthConfig); ok {
tx = tx.Where("u.deposit_free_quota_cent > 0 OR u.renter_growth_points >= ?", minPoints)
} else {
tx = tx.Where("u.deposit_free_quota_cent > 0")
}
}
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%"
@@ -89,6 +97,24 @@ func applyUserFilter(tx *gorm.DB, query ListQuery) *gorm.DB {
return tx
}
func minimumGrowthQuotaPoints(cfg rentergrowth.Config) (int64, bool) {
if !cfg.Enabled {
return 0, false
}
var minPoints int64
found := false
for _, level := range cfg.Levels {
if level.DepositFreeQuotaCent <= 0 {
continue
}
if !found || level.MinPoints < minPoints {
minPoints = level.MinPoints
found = true
}
}
return minPoints, found
}
func (r *Repository) Freeze(ctx context.Context, adminID uint64, userID uint64, req FreezeRequest, meta AuditMeta) (*UserDTO, error) {
return r.updateStatus(ctx, adminID, userID, "frozen", "frozen", "admin_user.freeze", req.Reason, meta)
}
@@ -124,6 +150,62 @@ func (r *Repository) SetDepositFreeQuota(ctx context.Context, adminID uint64, us
return r.Find(ctx, userID)
}
func (r *Repository) AdjustGrowthPoints(ctx context.Context, adminID uint64, userID uint64, req GrowthPointsAdjustRequest, meta AuditMeta) (*UserDTO, error) {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var user model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
return err
}
if user.RenterGrowthPoints == req.TargetPoints {
return ErrGrowthPointsUnchanged
}
cfg, err := rentergrowth.ConfigForTx(tx)
if err != nil {
return err
}
beforePoints := user.RenterGrowthPoints
beforeLevel := rentergrowth.LevelForPoints(cfg, beforePoints)
afterLevel := rentergrowth.LevelForPoints(cfg, req.TargetPoints)
if !cfg.Enabled {
beforeLevel = rentergrowth.LevelForPoints(cfg, 0)
afterLevel = beforeLevel
}
user.RenterGrowthPoints = req.TargetPoints
user.RenterGrowthLevel = afterLevel.Code
if err := tx.Save(&user).Error; err != nil {
return err
}
ledger := model.RenterGrowthLedger{
UserID: user.ID,
Points: req.TargetPoints - beforePoints,
BeforePoints: beforePoints,
AfterPoints: req.TargetPoints,
BeforeLevel: beforeLevel.Code,
AfterLevel: afterLevel.Code,
Source: rentergrowth.SourceAdminAdjustment,
OperatorAdminID: &adminID,
Remark: "后台调整成长积分: " + req.Reason,
CreatedAt: time.Now(),
}
if err := tx.Create(&ledger).Error; err != nil {
return err
}
return appendAuditLog(tx, adminID, "admin_user.adjust_growth_points", user.ID, meta, map[string]any{
"user_id": user.ID,
"before_points": beforePoints,
"after_points": req.TargetPoints,
"change_points": req.TargetPoints - beforePoints,
"before_level": beforeLevel.Code,
"after_level": afterLevel.Code,
"reason": req.Reason,
})
})
if err != nil {
return nil, err
}
return r.Find(ctx, userID)
}
func (r *Repository) updateStatus(ctx context.Context, adminID uint64, userID uint64, status string, riskStatus string, action string, reason string, meta AuditMeta) (*UserDTO, error) {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var user model.User
@@ -270,8 +352,12 @@ func (r *Repository) ManualRealname(ctx context.Context, adminID uint64, userID
}
func (r *Repository) Find(ctx context.Context, userID uint64) (*UserDTO, error) {
growthConfig, err := rentergrowth.ConfigForTx(r.db.WithContext(ctx))
if err != nil {
return nil, err
}
var row userRow
err := r.db.WithContext(ctx).Table("users AS u").
err = r.db.WithContext(ctx).Table("users AS u").
Select(`u.*,
COALESCE(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count,
@@ -289,7 +375,7 @@ func (r *Repository) Find(ctx context.Context, userID uint64) (*UserDTO, error)
if err != nil {
return nil, err
}
dto := row.toDTO()
dto := row.toDTO(growthConfig)
return &dto, nil
}
@@ -385,53 +471,43 @@ type userRow struct {
FrozenBalanceCent int64
}
func (row userRow) toDTO() UserDTO {
remaining := row.DepositFreeQuotaCent - row.DepositFreeUsedCent
func (row userRow) toDTO(growthConfig rentergrowth.Config) UserDTO {
level := rentergrowth.LevelForPoints(growthConfig, row.RenterGrowthPoints)
levelQuotaCent := level.DepositFreeQuotaCent
if !growthConfig.Enabled {
level = rentergrowth.LevelForPoints(growthConfig, 0)
levelQuotaCent = 0
}
effectiveQuotaCent := rentergrowth.EffectiveDepositFreeQuotaCent(levelQuotaCent, row.DepositFreeQuotaCent)
remaining := effectiveQuotaCent - row.DepositFreeUsedCent
if remaining < 0 {
remaining = 0
}
return UserDTO{
ID: row.ID,
Phone: row.Phone,
Nickname: row.Nickname,
RealnameStatus: row.RealnameStatus,
RiskStatus: row.RiskStatus,
CreditScore: row.CreditScore,
DepositFreeQuotaCent: row.DepositFreeQuotaCent,
DepositFreeUsedCent: row.DepositFreeUsedCent,
DepositFreeRemainingCent: remaining,
RenterGrowthPoints: row.RenterGrowthPoints,
RenterGrowthLevel: effectiveRenterGrowthLevel(row.User),
RenterGrowthLevelName: renterGrowthLevelName(row.RenterGrowthLevel),
AvailableBalanceCent: row.AvailableBalanceCent,
FrozenBalanceCent: row.FrozenBalanceCent,
Status: row.Status,
OrderCount: row.OrderCount,
ListingCount: row.ListingCount,
DisputeCount: row.DisputeCount,
LastLoginAt: row.LastLoginAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}
func effectiveRenterGrowthLevel(user model.User) string {
if user.RenterGrowthLevel != "" {
return user.RenterGrowthLevel
}
return rentergrowth.DefaultLevelCode
}
func renterGrowthLevelName(code string) string {
switch code {
case "platinum":
return "铂金"
case "diamond":
return "钻石"
case "peak":
return "巅峰"
default:
return rentergrowth.DefaultLevelName
ID: row.ID,
Phone: row.Phone,
Nickname: row.Nickname,
RealnameStatus: row.RealnameStatus,
RiskStatus: row.RiskStatus,
CreditScore: row.CreditScore,
DepositFreeQuotaCent: row.DepositFreeQuotaCent,
DepositFreeManualQuotaCent: row.DepositFreeQuotaCent,
DepositFreeLevelQuotaCent: levelQuotaCent,
DepositFreeEffectiveQuotaCent: effectiveQuotaCent,
DepositFreeUsedCent: row.DepositFreeUsedCent,
DepositFreeRemainingCent: remaining,
RenterGrowthPoints: row.RenterGrowthPoints,
RenterGrowthLevel: level.Code,
RenterGrowthLevelName: level.Name,
AvailableBalanceCent: row.AvailableBalanceCent,
FrozenBalanceCent: row.FrozenBalanceCent,
Status: row.Status,
OrderCount: row.OrderCount,
ListingCount: row.ListingCount,
DisputeCount: row.DisputeCount,
LastLoginAt: row.LastLoginAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}