package adminuser import ( "context" "errors" "strconv" "strings" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/model" "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(ctx context.Context, page, pageSize int, query ListQuery) (*PaginatedResult, error) { var total int64 countTx := applyUserFilter(r.db.WithContext(ctx).Table("users AS u"), query) if err := countTx.Count(&total).Error; err != nil { return nil, err } offset := (page - 1) * pageSize var rows []userRow listTx := r.db.WithContext(ctx).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_cent, 0) AS deposit_free_used_cent`). 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_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id") listTx = applyUserFilter(listTx, query) err := listTx. 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 } // applyUserFilter 将列表筛选条件应用到查询(基于 users 表别名 u)。 func applyUserFilter(tx *gorm.DB, query ListQuery) *gorm.DB { if query.Status != "" { tx = tx.Where("u.status = ?", query.Status) } if query.HasDepositFreeQuota { tx = tx.Where("u.deposit_free_quota_cent > 0") } if keyword := strings.TrimSpace(query.Keyword); keyword != "" { like := "%" + keyword + "%" // 关键词为纯数字时一并按用户 ID 精确匹配 if id, err := strconv.ParseUint(keyword, 10, 64); err == nil { tx = tx.Where("u.id = ? OR u.phone LIKE ? OR u.nickname LIKE ?", id, like, like) } else { tx = tx.Where("u.phone LIKE ? OR u.nickname LIKE ?", like, like) } } return tx } func (r *Repository) Freeze(ctx context.Context, adminID uint64, userID uint64, req FreezeRequest, meta AuditMeta) (*UserDTO, error) { return r.updateStatus(ctx, adminID, userID, "frozen", "frozen", "admin_user.freeze", req.Reason, meta) } func (r *Repository) Unfreeze(ctx context.Context, adminID uint64, userID uint64, meta AuditMeta) (*UserDTO, error) { return r.updateStatus(ctx, adminID, userID, "active", "normal", "admin_user.unfreeze", "", meta) } func (r *Repository) SetDepositFreeQuota(ctx context.Context, adminID uint64, userID uint64, req DepositFreeQuotaRequest, meta AuditMeta) (*UserDTO, error) { amountCent := depositFreeQuotaAmountCent(req) if amountCent < 0 { return nil, ErrInvalidUser } 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 } beforeAmountCent := user.DepositFreeQuotaCent user.DepositFreeQuotaCent = amountCent 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_cent": beforeAmountCent, "after_amount_cent": amountCent, }) }) 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) { 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 } 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(ctx, userID) } // RevokeRealname 撤销用户实名:将用户实名状态重置为未认证,并删除实名记录, // 使用户可重新发起实名认证(用于实名姓名与收款账户不一致导致收款码登记失败的场景)。 func (r *Repository) RevokeRealname(ctx context.Context, adminID uint64, userID uint64, req RevokeRealnameRequest, 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 } var realnameCount int64 if err := tx.Model(&model.UserRealname{}).Where("user_id = ?", userID).Count(&realnameCount).Error; err != nil { return err } // 已是未认证且无实名记录,无可撤销内容。 if user.RealnameStatus == "unverified" && realnameCount == 0 { return ErrRealnameNotRevocable } beforeRealnameStatus := user.RealnameStatus user.RealnameStatus = "unverified" if err := tx.Save(&user).Error; err != nil { return err } if err := tx.Where("user_id = ?", userID).Delete(&model.UserRealname{}).Error; err != nil { return err } return appendAuditLog(tx, adminID, "admin_user.revoke_realname", user.ID, meta, map[string]any{ "user_id": user.ID, "reason": req.Reason, "before_realname_status": beforeRealnameStatus, "after_realname_status": user.RealnameStatus, }) }) if err != nil { return nil, err } return r.Find(ctx, userID) } func (r *Repository) Find(ctx context.Context, userID uint64) (*UserDTO, error) { var row userRow err := r.db.WithContext(ctx).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_cent, 0) AS deposit_free_used_cent`). 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_cent) AS deposit_free_used_cent 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 DepositFreeUsedCent int64 } func (row userRow) toDTO() UserDTO { remaining := row.DepositFreeQuotaCent - row.DepositFreeUsedCent 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, DepositFreeQuotaCent: row.DepositFreeQuotaCent, DepositFreeUsedCent: row.DepositFreeUsedCent, DepositFreeRemainingCent: remaining, Status: row.Status, OrderCount: row.OrderCount, ListingCount: row.ListingCount, DisputeCount: row.DisputeCount, LastLoginAt: row.LastLoginAt, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func depositFreeQuotaAmountCent(req DepositFreeQuotaRequest) int64 { return req.AmountCent } 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) }