diff --git a/backend/internal/modules/adminuser/dto.go b/backend/internal/modules/adminuser/dto.go index 559d58b..391a212 100644 --- a/backend/internal/modules/adminuser/dto.go +++ b/backend/internal/modules/adminuser/dto.go @@ -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"` diff --git a/backend/internal/modules/adminuser/handler.go b/backend/internal/modules/adminuser/handler.go index 4abd4bd..ffd48c3 100644 --- a/backend/internal/modules/adminuser/handler.go +++ b/backend/internal/modules/adminuser/handler.go @@ -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 diff --git a/backend/internal/modules/adminuser/repository.go b/backend/internal/modules/adminuser/repository.go index bf33ee4..2cb0327 100644 --- a/backend/internal/modules/adminuser/repository.go +++ b/backend/internal/modules/adminuser/repository.go @@ -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) } diff --git a/backend/internal/modules/adminuser/service.go b/backend/internal/modules/adminuser/service.go index d72c126..8302fe5 100644 --- a/backend/internal/modules/adminuser/service.go +++ b/backend/internal/modules/adminuser/service.go @@ -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) { diff --git a/frontend/src/features/admin/api/adminUsers.ts b/frontend/src/features/admin/api/adminUsers.ts index ffea016..10a61c9 100644 --- a/frontend/src/features/admin/api/adminUsers.ts +++ b/frontend/src/features/admin/api/adminUsers.ts @@ -22,13 +22,18 @@ export interface AdminUserItem { updated_at: string } -export async function fetchAdminUsers(page = 1, pageSize = 20) { - const { data } = await apiClient.get>>( - '/admin/users', - { - params: { page, page_size: pageSize }, - } - ) +export interface AdminUserQuery { + keyword?: string + status?: '' | UserStatus +} + +export async function fetchAdminUsers(page = 1, pageSize = 20, query: AdminUserQuery = {}) { + const params: Record = { page, page_size: pageSize } + if (query.keyword) params.keyword = query.keyword + if (query.status) params.status = query.status + const { data } = await apiClient.get>>('/admin/users', { + params, + }) return data.data } diff --git a/frontend/src/features/admin/views/AdminUsersView.vue b/frontend/src/features/admin/views/AdminUsersView.vue index 6a7dbdc..f9ccb3f 100644 --- a/frontend/src/features/admin/views/AdminUsersView.vue +++ b/frontend/src/features/admin/views/AdminUsersView.vue @@ -1,7 +1,8 @@