完善租客等级免押与积分管理
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user