package adminuser import ( "errors" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/model" "hfb_sys/backend/pkg/money" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } type AuditMeta = auditlog.Meta func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } func (r *Repository) List(page, pageSize int) (*PaginatedResult, error) { var total int64 if err := r.db.Model(&model.User{}).Count(&total).Error; err != nil { return nil, err } offset := (page - 1) * pageSize var rows []userRow err := r.db.Table("users AS u"). Select(`u.*, COALESCE(o.order_count, 0) AS order_count, COALESCE(l.listing_count, 0) AS listing_count, COALESCE(d.dispute_count, 0) AS dispute_count, COALESCE(df.deposit_free_used, 0) AS deposit_free_used`). Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id"). Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id"). 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) AS deposit_free_used FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id"). Order("u.id DESC"). Offset(offset).Limit(pageSize). Scan(&rows).Error if err != nil { return nil, err } items := make([]UserDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } func (r *Repository) Freeze(adminID uint64, userID uint64, req FreezeRequest, meta AuditMeta) (*UserDTO, error) { return r.updateStatus(adminID, userID, "frozen", "frozen", "admin_user.freeze", req.Reason, meta) } func (r *Repository) Unfreeze(adminID uint64, userID uint64, meta AuditMeta) (*UserDTO, error) { return r.updateStatus(adminID, userID, "active", "normal", "admin_user.unfreeze", "", meta) } func (r *Repository) SetDepositFreeQuota(adminID uint64, userID uint64, req DepositFreeQuotaRequest, meta AuditMeta) (*UserDTO, error) { amount := roundMoney(req.Amount) if amount < 0 { return nil, ErrInvalidUser } err := r.db.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 } beforeAmount := user.DepositFreeQuota user.DepositFreeQuota = amount if err := tx.Save(&user).Error; err != nil { return err } return appendAuditLog(tx, adminID, "admin_user.set_deposit_free_quota", user.ID, meta, map[string]any{ "user_id": user.ID, "before_amount": beforeAmount, "after_amount": amount, }) }) if err != nil { return nil, err } return r.Find(userID) } func (r *Repository) updateStatus(adminID uint64, userID uint64, status string, riskStatus string, action string, reason string, meta AuditMeta) (*UserDTO, error) { err := r.db.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 } beforeStatus := user.Status beforeRisk := user.RiskStatus user.Status = status user.RiskStatus = riskStatus if err := tx.Save(&user).Error; err != nil { return err } return appendAuditLog(tx, adminID, action, user.ID, meta, map[string]any{ "user_id": user.ID, "reason": reason, "before_status": beforeStatus, "after_status": status, "before_risk_status": beforeRisk, "after_risk_status": riskStatus, }) }) if err != nil { return nil, err } return r.Find(userID) } func (r *Repository) Find(userID uint64) (*UserDTO, error) { var row userRow err := r.db.Table("users AS u"). Select(`u.*, COALESCE(o.order_count, 0) AS order_count, COALESCE(l.listing_count, 0) AS listing_count, COALESCE(d.dispute_count, 0) AS dispute_count, COALESCE(df.deposit_free_used, 0) AS deposit_free_used`). Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id"). Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id"). 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) AS deposit_free_used FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id"). Where("u.id = ?", userID). First(&row).Error if err != nil { return nil, err } dto := row.toDTO() return &dto, nil } type userRow struct { model.User OrderCount int64 ListingCount int64 DisputeCount int64 DepositFreeUsed float64 } func (row userRow) toDTO() UserDTO { remaining := roundMoney(row.DepositFreeQuota - row.DepositFreeUsed) 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, DepositFreeQuota: row.DepositFreeQuota, DepositFreeUsed: roundMoney(row.DepositFreeUsed), DepositFreeRemaining: remaining, Status: row.Status, OrderCount: row.OrderCount, ListingCount: row.ListingCount, DisputeCount: row.DisputeCount, LastLoginAt: row.LastLoginAt, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func roundMoney(value float64) float64 { return money.Round(value) } func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error { return auditlog.Append(tx, auditlog.Entry{ ActorType: "admin", ActorID: actorID, Action: action, BizType: "user", BizID: &bizID, Meta: meta, Detail: detail, }) } func IsNotFound(err error) bool { return errors.Is(err, gorm.ErrRecordNotFound) }