feat(adminuser): 优化用户管理筛选并展示筛选人数

- 免押额度与号主类型改为下拉筛选,支持有/无和优质/普通
- 新增用户成长等级筛选
- 列表上方固定展示筛选结果人数,补充组合筛选测试
This commit is contained in:
yml2213
2026-11-06 19:09:39 +08:00
parent 87d252bbee
commit d39f45621b
7 changed files with 259 additions and 29 deletions
@@ -78,6 +78,12 @@ type ListQuery struct {
Status string
HasDepositFreeQuota bool
QualityOwnerOnly bool
// DepositFreeQuota: has=有免押额度, none=无免押额度, 空=全部。
DepositFreeQuota string
// OwnerType: quality=优质号主, normal=普通用户, 空=全部。
OwnerType string
// GrowthLevel: 成长等级 code(normal/platinum/diamond/peak),空=全部。
GrowthLevel string
}
type PaginatedResult struct {
@@ -42,6 +42,9 @@ func (h *Handler) List(c *gin.Context) {
Status: strings.TrimSpace(c.Query("status")),
HasDepositFreeQuota: parseBoolQuery(c.Query("has_deposit_free_quota")),
QualityOwnerOnly: parseBoolQuery(c.Query("quality_owner_only")),
DepositFreeQuota: normalizeEnumQuery(c.Query("deposit_free_quota"), "has", "none"),
OwnerType: normalizeEnumQuery(c.Query("owner_type"), "quality", "normal"),
GrowthLevel: strings.TrimSpace(c.Query("growth_level")),
}
result, err := h.service.List(c.Request.Context(), page, pageSize, query)
if err != nil {
@@ -60,6 +63,16 @@ func parseBoolQuery(value string) bool {
}
}
func normalizeEnumQuery(value string, allowed ...string) string {
value = strings.ToLower(strings.TrimSpace(value))
for _, item := range allowed {
if value == item {
return value
}
}
return ""
}
func (h *Handler) Freeze(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
@@ -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,25 +104,30 @@ func applyUserFilter(tx *gorm.DB, query ListQuery, growthConfig rentergrowth.Con
if query.Status != "" {
tx = tx.Where("u.status = ?", query.Status)
}
if query.HasDepositFreeQuota {
quotaLevelCodes := growthQuotaLevelCodes(growthConfig)
if minPoints, ok := minimumGrowthQuotaPoints(growthConfig); ok {
tx = tx.Where(
"u.deposit_free_quota_cent > 0 OR u.renter_growth_points >= ? OR u.renter_growth_level_override IN ?",
minPoints,
quotaLevelCodes,
)
} else if len(quotaLevelCodes) > 0 {
tx = tx.Where(
"u.deposit_free_quota_cent > 0 OR u.renter_growth_level_override IN ?",
quotaLevelCodes,
)
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...)
}
}
if query.QualityOwnerOnly {
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) {
@@ -135,6 +145,21 @@ 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 {
@@ -85,6 +85,128 @@ func TestApplyUserFilterHasDepositFreeQuota(t *testing.T) {
}
}
func TestApplyUserFilterNoneDepositFreeQuota(t *testing.T) {
db := database.NewDryRunDB()
stmt := applyUserFilter(db.Table("users AS u"), ListQuery{
DepositFreeQuota: "none",
}, rentergrowth.DefaultConfig()).Find(&[]struct{}{}).Statement
sql := stmt.SQL.String()
if !strings.Contains(sql, "NOT (u.deposit_free_quota_cent > 0 OR u.renter_growth_points >= ? OR u.renter_growth_level_override IN (?,?,?))") {
t.Fatalf("SQL 未包含无免押额度过滤:%s", sql)
}
}
func TestApplyUserFilterOwnerTypeAndGrowthLevel(t *testing.T) {
db := database.NewDryRunDB()
stmt := applyUserFilter(db.Table("users AS u"), ListQuery{
OwnerType: "normal",
GrowthLevel: "diamond",
}, rentergrowth.DefaultConfig()).Find(&[]struct{}{}).Statement
sql := stmt.SQL.String()
if !strings.Contains(sql, "u.quality_owner_status <> ?") {
t.Fatalf("SQL 未包含普通号主过滤:%s", sql)
}
if !strings.Contains(sql, "u.renter_growth_level = ?") {
t.Fatalf("SQL 未包含成长等级过滤:%s", sql)
}
if len(stmt.Vars) != 2 || stmt.Vars[0] != "active" || stmt.Vars[1] != "diamond" {
t.Fatalf("筛选变量不正确:%v", stmt.Vars)
}
}
func TestNormalizeEnumQuery(t *testing.T) {
tests := []struct {
input string
want string
}{
{input: " QUALITY ", want: "quality"},
{input: "normal", want: "normal"},
{input: "unknown", want: ""},
{input: "", want: ""},
}
for _, tt := range tests {
if got := normalizeEnumQuery(tt.input, "quality", "normal"); got != tt.want {
t.Fatalf("normalizeEnumQuery(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestListFiltersByDepositQuotaOwnerTypeAndGrowthLevel(t *testing.T) {
db := setupAdminUserTestDB(t)
users := []model.User{
{Phone: "13900001001", Nickname: "普通无免押", Status: "active", QualityOwnerStatus: "active", RenterGrowthLevel: "normal"},
{Phone: "13900001002", Nickname: "铂金有免押", Status: "active", QualityOwnerStatus: "none", RenterGrowthPoints: 300, RenterGrowthLevel: "platinum"},
{Phone: "13900001003", Nickname: "钻石有免押", Status: "active", QualityOwnerStatus: "none", RenterGrowthPoints: 1000, RenterGrowthLevel: "diamond"},
{Phone: "13900001004", Nickname: "人工免押", Status: "active", QualityOwnerStatus: "none", DepositFreeQuotaCent: 20000, RenterGrowthLevel: "normal"},
}
if err := db.Create(&users).Error; err != nil {
t.Fatalf("创建用户失败:%v", err)
}
repo := NewRepository(db)
tests := []struct {
name string
query ListQuery
wantIDs []uint64
}{
{
name: "有免押额度",
query: ListQuery{DepositFreeQuota: "has"},
wantIDs: []uint64{users[1].ID, users[2].ID, users[3].ID},
},
{
name: "无免押额度",
query: ListQuery{DepositFreeQuota: "none"},
wantIDs: []uint64{users[0].ID},
},
{
name: "优质号主",
query: ListQuery{OwnerType: "quality"},
wantIDs: []uint64{users[0].ID},
},
{
name: "普通用户",
query: ListQuery{OwnerType: "normal"},
wantIDs: []uint64{users[1].ID, users[2].ID, users[3].ID},
},
{
name: "铂金等级",
query: ListQuery{GrowthLevel: "platinum"},
wantIDs: []uint64{users[1].ID},
},
{
name: "组合筛选",
query: ListQuery{OwnerType: "normal", GrowthLevel: "normal", DepositFreeQuota: "has"},
wantIDs: []uint64{users[3].ID},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := repo.List(t.Context(), 1, 20, tt.query)
if err != nil {
t.Fatalf("List() error = %v", err)
}
if result.Total != int64(len(tt.wantIDs)) {
t.Fatalf("Total = %d, want %d", result.Total, len(tt.wantIDs))
}
items := result.Items.([]UserDTO)
got := make(map[uint64]bool, len(items))
for _, item := range items {
got[item.ID] = true
}
for _, id := range tt.wantIDs {
if !got[id] {
t.Fatalf("结果缺少用户 %d:%v", id, got)
}
}
})
}
}
func TestAdjustGrowthPointsWritesRepeatableLedgers(t *testing.T) {
db := setupAdminUserTestDB(t)
user := model.User{