feat: 接入旧站管理端并完整迁移后台能力
This commit is contained in:
@@ -52,6 +52,11 @@ func (r *Repository) List(ctx context.Context, page, pageSize int, query ListQue
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if query.GrowthLevel != "" {
|
||||
if _, ok := rentergrowth.LevelForCode(growthConfig, query.GrowthLevel); !ok {
|
||||
return nil, ErrInvalidGrowthLevel
|
||||
}
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []userRow
|
||||
listTx := r.db.WithContext(ctx).Table("users AS u").
|
||||
@@ -99,13 +104,31 @@ func applyUserFilter(tx *gorm.DB, query ListQuery, growthConfig rentergrowth.Con
|
||||
if query.Status != "" {
|
||||
tx = tx.Where("u.status = ?", query.Status)
|
||||
}
|
||||
if query.HasDepositFreeQuota {
|
||||
if minPoints, ok := minimumGrowthQuotaPoints(growthConfig); ok {
|
||||
tx = tx.Where("u.deposit_free_quota_cent > 0 OR u.renter_growth_points >= ?", minPoints)
|
||||
depositFreeQuota := query.DepositFreeQuota
|
||||
if depositFreeQuota == "" && query.HasDepositFreeQuota {
|
||||
depositFreeQuota = "has"
|
||||
}
|
||||
if depositFreeQuota != "" {
|
||||
condition, args := depositFreeQuotaCondition(growthConfig)
|
||||
if depositFreeQuota == "none" {
|
||||
tx = tx.Where("NOT ("+condition+")", args...)
|
||||
} else {
|
||||
tx = tx.Where("u.deposit_free_quota_cent > 0")
|
||||
tx = tx.Where(condition, args...)
|
||||
}
|
||||
}
|
||||
ownerType := query.OwnerType
|
||||
if ownerType == "" && query.QualityOwnerOnly {
|
||||
ownerType = "quality"
|
||||
}
|
||||
switch ownerType {
|
||||
case "quality":
|
||||
tx = tx.Where("u.quality_owner_status = ?", "active")
|
||||
case "normal":
|
||||
tx = tx.Where("u.quality_owner_status <> ?", "active")
|
||||
}
|
||||
if query.GrowthLevel != "" {
|
||||
tx = tx.Where("u.renter_growth_level = ?", query.GrowthLevel)
|
||||
}
|
||||
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
||||
if len(keyword) == 11 && keyword[0] == '1' && isAllDigits(keyword) {
|
||||
// 11 位手机号:命中 users(phone) 唯一索引
|
||||
@@ -122,6 +145,31 @@ func applyUserFilter(tx *gorm.DB, query ListQuery, growthConfig rentergrowth.Con
|
||||
return tx
|
||||
}
|
||||
|
||||
// depositFreeQuotaCondition 返回“有免押额度”的 SQL 条件:
|
||||
// 人工额度 > 0,或当前成长等级自带免押额度。
|
||||
func depositFreeQuotaCondition(cfg rentergrowth.Config) (string, []any) {
|
||||
quotaLevelCodes := growthQuotaLevelCodes(cfg)
|
||||
if minPoints, ok := minimumGrowthQuotaPoints(cfg); ok {
|
||||
return "u.deposit_free_quota_cent > 0 OR u.renter_growth_points >= ? OR u.renter_growth_level_override IN ?",
|
||||
[]any{minPoints, quotaLevelCodes}
|
||||
}
|
||||
if len(quotaLevelCodes) > 0 {
|
||||
return "u.deposit_free_quota_cent > 0 OR u.renter_growth_level_override IN ?",
|
||||
[]any{quotaLevelCodes}
|
||||
}
|
||||
return "u.deposit_free_quota_cent > 0", nil
|
||||
}
|
||||
|
||||
func growthQuotaLevelCodes(cfg rentergrowth.Config) []string {
|
||||
codes := make([]string, 0, len(cfg.Levels))
|
||||
for _, level := range cfg.Levels {
|
||||
if level.DepositFreeQuotaCent > 0 {
|
||||
codes = append(codes, level.Code)
|
||||
}
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func minimumGrowthQuotaPoints(cfg rentergrowth.Config) (int64, bool) {
|
||||
if !cfg.Enabled {
|
||||
return 0, false
|
||||
@@ -189,28 +237,39 @@ func (r *Repository) AdjustGrowthPoints(ctx context.Context, adminID uint64, use
|
||||
return err
|
||||
}
|
||||
beforePoints := user.RenterGrowthPoints
|
||||
beforeLevel := rentergrowth.LevelForPoints(cfg, beforePoints)
|
||||
afterLevel := rentergrowth.LevelForPoints(cfg, req.TargetPoints)
|
||||
if !cfg.Enabled {
|
||||
beforeAvailable := user.PointsAvailable
|
||||
pointsDelta := req.TargetPoints - beforePoints
|
||||
afterAvailable := beforeAvailable + pointsDelta
|
||||
if afterAvailable < 0 {
|
||||
afterAvailable = 0
|
||||
}
|
||||
beforeLevel := rentergrowth.EffectiveLevelForUser(cfg, beforePoints, user.RenterGrowthLevelOverride)
|
||||
afterLevel := rentergrowth.LevelForUserAfterPoints(cfg, req.TargetPoints, user.RenterGrowthLevelOverride)
|
||||
if !cfg.Enabled && user.RenterGrowthLevelOverride == "" {
|
||||
beforeLevel = rentergrowth.LevelForPoints(cfg, 0)
|
||||
afterLevel = beforeLevel
|
||||
}
|
||||
user.RenterGrowthPoints = req.TargetPoints
|
||||
user.RenterGrowthLevel = afterLevel.Code
|
||||
user.PointsAvailable = afterAvailable
|
||||
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(),
|
||||
UserID: user.ID,
|
||||
Role: "admin",
|
||||
Points: pointsDelta,
|
||||
PointsMultiplierBps: afterLevel.PointsMultiplierBps,
|
||||
BeforePoints: beforePoints,
|
||||
AfterPoints: req.TargetPoints,
|
||||
AvailableBefore: beforeAvailable,
|
||||
AvailableAfter: afterAvailable,
|
||||
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
|
||||
@@ -219,7 +278,7 @@ func (r *Repository) AdjustGrowthPoints(ctx context.Context, adminID uint64, use
|
||||
"user_id": user.ID,
|
||||
"before_points": beforePoints,
|
||||
"after_points": req.TargetPoints,
|
||||
"change_points": req.TargetPoints - beforePoints,
|
||||
"change_points": pointsDelta,
|
||||
"before_level": beforeLevel.Code,
|
||||
"after_level": afterLevel.Code,
|
||||
"reason": req.Reason,
|
||||
@@ -231,6 +290,56 @@ func (r *Repository) AdjustGrowthPoints(ctx context.Context, adminID uint64, use
|
||||
return r.Find(ctx, userID)
|
||||
}
|
||||
|
||||
func (r *Repository) AdjustGrowthLevel(ctx context.Context, adminID uint64, userID uint64, req GrowthLevelAdjustRequest, 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
|
||||
}
|
||||
cfg, err := rentergrowth.ConfigForTx(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetLevel := strings.TrimSpace(req.TargetLevel)
|
||||
beforeOverride := user.RenterGrowthLevelOverride
|
||||
beforeLevel := rentergrowth.EffectiveLevelForUser(cfg, user.RenterGrowthPoints, beforeOverride)
|
||||
if beforeOverride == "" && !cfg.Enabled {
|
||||
beforeLevel = rentergrowth.LevelForPoints(cfg, 0)
|
||||
}
|
||||
afterLevel := rentergrowth.LevelForPoints(cfg, user.RenterGrowthPoints)
|
||||
if targetLevel != "" {
|
||||
level, ok := rentergrowth.LevelForCode(cfg, targetLevel)
|
||||
if !ok {
|
||||
return ErrInvalidGrowthLevel
|
||||
}
|
||||
afterLevel = level
|
||||
} else if !cfg.Enabled {
|
||||
afterLevel = rentergrowth.LevelForPoints(cfg, 0)
|
||||
}
|
||||
if targetLevel == beforeOverride && user.RenterGrowthLevel == afterLevel.Code {
|
||||
return ErrGrowthLevelUnchanged
|
||||
}
|
||||
|
||||
user.RenterGrowthLevelOverride = targetLevel
|
||||
user.RenterGrowthLevel = afterLevel.Code
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return appendAuditLog(tx, adminID, "admin_user.adjust_growth_level", user.ID, meta, map[string]any{
|
||||
"user_id": user.ID,
|
||||
"before_level": beforeLevel.Code,
|
||||
"after_level": afterLevel.Code,
|
||||
"before_override": beforeOverride,
|
||||
"after_override": targetLevel,
|
||||
"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) {
|
||||
statusChanged := false
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
@@ -534,9 +643,13 @@ type userRow struct {
|
||||
}
|
||||
|
||||
func (row userRow) toDTO(growthConfig rentergrowth.Config) UserDTO {
|
||||
level := rentergrowth.LevelForPoints(growthConfig, row.RenterGrowthPoints)
|
||||
level := rentergrowth.EffectiveLevelForUser(
|
||||
growthConfig,
|
||||
row.RenterGrowthPoints,
|
||||
row.RenterGrowthLevelOverride,
|
||||
)
|
||||
levelQuotaCent := level.DepositFreeQuotaCent
|
||||
if !growthConfig.Enabled {
|
||||
if !growthConfig.Enabled && row.RenterGrowthLevelOverride == "" {
|
||||
level = rentergrowth.LevelForPoints(growthConfig, 0)
|
||||
levelQuotaCent = 0
|
||||
}
|
||||
@@ -559,10 +672,17 @@ func (row userRow) toDTO(growthConfig rentergrowth.Config) UserDTO {
|
||||
DepositFreeUsedCent: row.DepositFreeUsedCent,
|
||||
DepositFreeRemainingCent: remaining,
|
||||
RenterGrowthPoints: row.RenterGrowthPoints,
|
||||
PointsAvailable: row.PointsAvailable,
|
||||
RenterGrowthLevel: level.Code,
|
||||
RenterGrowthLevelName: level.Name,
|
||||
RenterGrowthLevelOverride: row.RenterGrowthLevelOverride,
|
||||
RenterGrowthLevelManual: row.RenterGrowthLevelOverride != "",
|
||||
AvailableBalanceCent: row.AvailableBalanceCent,
|
||||
FrozenBalanceCent: row.FrozenBalanceCent,
|
||||
QualityOwnerStatus: row.QualityOwnerStatus,
|
||||
QualityOwnerDepositCent: row.QualityOwnerDepositCent,
|
||||
QualityOwnerDepositSource: row.QualityOwnerDepositSource,
|
||||
QualityOwnerActivatedAt: row.QualityOwnerActivatedAt,
|
||||
Status: row.Status,
|
||||
OrderCount: row.OrderCount,
|
||||
ListingCount: row.ListingCount,
|
||||
|
||||
Reference in New Issue
Block a user