feat: 接入旧站管理端并完整迁移后台能力

This commit is contained in:
yml
2026-11-12 22:03:01 +08:00
parent e5f923706b
commit a4441bba9a
509 changed files with 140714 additions and 1068 deletions
+140 -20
View File
@@ -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,