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
@@ -73,9 +73,138 @@ func TestApplyUserFilterHasDepositFreeQuota(t *testing.T) {
if !strings.Contains(sql, "u.deposit_free_quota_cent > 0 OR u.renter_growth_points >=") {
t.Fatalf("SQL 未包含免押额度过滤:%s", sql)
}
if len(stmt.Vars) != 2 || stmt.Vars[0] != "active" || stmt.Vars[1] != int64(300) {
if len(stmt.Vars) != 5 || stmt.Vars[0] != "active" || stmt.Vars[1] != int64(300) {
t.Fatalf("筛选变量不正确:%v", stmt.Vars)
}
overrideCodes := stmt.Vars[2:]
if len(overrideCodes) != 3 ||
overrideCodes[0] != "platinum" ||
overrideCodes[1] != "diamond" ||
overrideCodes[2] != "peak" {
t.Fatalf("手动等级筛选变量不正确:%v", overrideCodes)
}
}
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) {
@@ -120,6 +249,65 @@ func TestAdjustGrowthPointsWritesRepeatableLedgers(t *testing.T) {
}
}
func TestAdjustGrowthLevelLocksLevelAndWritesAudit(t *testing.T) {
db := setupAdminUserTestDB(t)
user := model.User{
Phone: "13900000006",
Status: "active",
RenterGrowthPoints: 100,
RenterGrowthLevel: rentergrowth.DefaultLevelCode,
}
if err := db.Create(&user).Error; err != nil {
t.Fatalf("创建用户失败:%v", err)
}
repo := NewRepository(db)
got, err := repo.AdjustGrowthLevel(t.Context(), 66, user.ID, GrowthLevelAdjustRequest{
TargetLevel: "platinum",
Reason: "客服核验后手动调整",
}, AuditMeta{RequestID: "level-1"})
if err != nil {
t.Fatalf("手动调整等级失败:%v", err)
}
if got.RenterGrowthLevel != "platinum" || !got.RenterGrowthLevelManual ||
got.RenterGrowthLevelOverride != "platinum" {
t.Fatalf("手动等级结果不正确:%+v", got)
}
got, err = repo.AdjustGrowthPoints(t.Context(), 66, user.ID, GrowthPointsAdjustRequest{
TargetPoints: 1000,
Reason: "补充成长积分",
}, AuditMeta{RequestID: "points-after-level"})
if err != nil {
t.Fatalf("调整积分失败:%v", err)
}
if got.RenterGrowthLevel != "platinum" || got.RenterGrowthLevelOverride != "platinum" {
t.Fatalf("积分调整不应覆盖手动等级:%+v", got)
}
got, err = repo.AdjustGrowthLevel(t.Context(), 66, user.ID, GrowthLevelAdjustRequest{
TargetLevel: "",
Reason: "恢复积分自动计算",
}, AuditMeta{RequestID: "level-auto"})
if err != nil {
t.Fatalf("恢复自动等级失败:%v", err)
}
if got.RenterGrowthLevel != "diamond" || got.RenterGrowthLevelManual ||
got.RenterGrowthLevelOverride != "" {
t.Fatalf("恢复自动等级结果不正确:%+v", got)
}
var auditCount int64
if err := db.Model(&model.AuditLog{}).
Where("action = ? AND biz_id = ?", "admin_user.adjust_growth_level", user.ID).
Count(&auditCount).Error; err != nil {
t.Fatalf("查询审计日志失败:%v", err)
}
if auditCount != 2 {
t.Fatalf("等级调整审计日志数量 = %d, want 2", auditCount)
}
}
func TestManualRealnameUpdatesUserRecordAndAudit(t *testing.T) {
db := setupAdminUserTestDB(t)
user := model.User{