用户管理增加查找筛选

后台用户列表此前仅分页,数据量大时无法定位用户。

- 后端:List 新增 ListQuery(关键词 + 状态),抽出 applyUserFilter
  对总数与列表查询统一加筛选;关键词为纯数字时同时按用户 ID 精确匹配,
  否则按手机号/昵称模糊匹配
- 前端:AdminUsersView 新增筛选栏(关键词:手机号/昵称/用户ID,
  状态:正常/已冻结/已禁用),回车/清空/选择即查,查询重置到第 1 页

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml2213
2026-06-17 00:58:14 +08:00
co-authored by Claude Opus 4.8
parent e75a39b03a
commit a88008f783
6 changed files with 120 additions and 17 deletions
@@ -28,6 +28,12 @@ type DepositFreeQuotaRequest struct {
AmountCent int64 `json:"amount_cent"`
}
// ListQuery 用户列表筛选条件。Keyword 同时匹配手机号/昵称(模糊)与用户 ID(精确)。
type ListQuery struct {
Keyword string
Status string
}
type PaginatedResult struct {
Items interface{} `json:"items"`
Total int64 `json:"total"`
@@ -4,6 +4,7 @@ import (
"errors"
"net/http"
"strconv"
"strings"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/pkg/response"
@@ -36,7 +37,11 @@ func parsePagination(c *gin.Context) (int, int) {
func (h *Handler) List(c *gin.Context) {
page, pageSize := parsePagination(c)
result, err := h.service.List(c.Request.Context(), page, pageSize)
query := ListQuery{
Keyword: strings.TrimSpace(c.Query("keyword")),
Status: strings.TrimSpace(c.Query("status")),
}
result, err := h.service.List(c.Request.Context(), page, pageSize, query)
if err != nil {
writeAdminUserError(c, err)
return
@@ -3,6 +3,8 @@ package adminuser
import (
"context"
"errors"
"strconv"
"strings"
"hfb_sys/backend/internal/auditlog"
"hfb_sys/backend/internal/model"
@@ -21,14 +23,15 @@ func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) List(ctx context.Context, page, pageSize int) (*PaginatedResult, error) {
func (r *Repository) List(ctx context.Context, page, pageSize int, query ListQuery) (*PaginatedResult, error) {
var total int64
if err := r.db.WithContext(ctx).Model(&model.User{}).Count(&total).Error; err != nil {
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
err := r.db.WithContext(ctx).Table("users AS u").
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,
@@ -37,7 +40,9 @@ func (r *Repository) List(ctx context.Context, page, pageSize int) (*PaginatedRe
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").
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
@@ -51,6 +56,23 @@ func (r *Repository) List(ctx context.Context, page, pageSize int) (*PaginatedRe
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 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)
}
@@ -18,11 +18,11 @@ func NewService(repo *Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) List(ctx context.Context, page, pageSize int) (*PaginatedResult, error) {
func (s *Service) List(ctx context.Context, page, pageSize int, query ListQuery) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.List(ctx, page, pageSize)
return s.repo.List(ctx, page, pageSize, query)
}
func (s *Service) Freeze(ctx context.Context, adminID uint64, userID uint64, req FreezeRequest, meta AuditMeta) (*UserDTO, error) {