diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index cb53715..f3b2abe 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -9,7 +9,8 @@ import { Elysia } from 'elysia'; import { userService } from './user.service'; -import { GetCurrentUserResponsesSchema } from './user.response'; +import { GetCurrentUserResponsesSchema, GetUserListResponsesSchema } from './user.response'; +import { UserListQuerySchema } from './user.schema'; import { tags } from '@/modules/tags'; import { jwtAuthPlugin } from '@/plugins/jwt/jwt.plugins'; import type { JwtUserType } from '@/type/jwt.type'; @@ -38,4 +39,24 @@ export const userController = new Elysia() }, response: GetCurrentUserResponsesSchema, } + ) + /** + * 用户列表查询接口 + * @route GET /api/users + * @description 获取用户列表,支持分页、搜索、筛选等功能,需要JWT认证 + */ + .get( + '/list', + ({ query }) => userService.getUserList(query), + { + query: UserListQuerySchema, + detail: { + summary: '获取用户列表', + description: '获取用户列表,支持分页查询、关键词搜索、状态筛选、排序等功能', + tags: [tags.user], + operationId: 'getUserList', + security: [{ bearerAuth: [] }] + }, + response: GetUserListResponsesSchema, + } ); \ No newline at end of file diff --git a/src/modules/user/user.response.ts b/src/modules/user/user.response.ts index 49ff10f..98ff804 100644 --- a/src/modules/user/user.response.ts +++ b/src/modules/user/user.response.ts @@ -4,12 +4,12 @@ * @date 2024-12-19 * @lastEditor AI Assistant * @lastEditTime 2025-01-07 - * @description 定义用户模块的响应格式,包括获取当前用户信息等 + * @description 定义用户模块的响应格式,包括获取当前用户信息、用户列表查询等 */ import { t, type Static } from 'elysia'; import { responseWrapperSchema } from '@/utils/responseFormate'; -import { CurrentUserSchema } from './user.schema'; +import { CurrentUserSchema, UserListResponseSchema } from './user.schema'; /** * 获取当前用户信息接口响应组合 @@ -37,5 +37,40 @@ export const GetCurrentUserResponsesSchema = { })) }; +/** + * 获取用户列表接口响应组合 + * @description 用于Controller中定义所有可能的响应格式 + */ +export const GetUserListResponsesSchema = { + 200: responseWrapperSchema(UserListResponseSchema), + 401: responseWrapperSchema(t.Object({ + error: t.String({ + description: '认证失败', + examples: ['未提供有效的认证令牌', '令牌已过期'] + }) + })), + 403: responseWrapperSchema(t.Object({ + error: t.String({ + description: '权限不足', + examples: ['权限不足,无法访问用户列表'] + }) + })), + 400: responseWrapperSchema(t.Object({ + error: t.String({ + description: '参数错误', + examples: ['分页参数无效', '搜索关键词格式错误'] + }) + })), + 500: responseWrapperSchema(t.Object({ + error: t.String({ + description: '服务器错误', + examples: ['内部服务器错误'] + }) + })) +}; + /** 获取当前用户信息成功响应数据类型 */ -export type GetCurrentUserSuccessType = Static; \ No newline at end of file +export type GetCurrentUserSuccessType = Static; + +/** 获取用户列表成功响应数据类型 */ +export type GetUserListSuccessType = Static; \ No newline at end of file diff --git a/src/modules/user/user.schema.ts b/src/modules/user/user.schema.ts index 900457b..a1c3711 100644 --- a/src/modules/user/user.schema.ts +++ b/src/modules/user/user.schema.ts @@ -4,10 +4,11 @@ * @date 2024-12-19 * @lastEditor AI Assistant * @lastEditTime 2025-01-07 - * @description 定义用户模块的Schema,包括获取当前用户信息等 + * @description 定义用户模块的Schema,包括获取当前用户信息、用户列表查询等 */ import { t, type Static } from 'elysia'; +import { createPaginationResponseSchema, createQuerySchema } from '@/utils/pagination'; /** * 当前用户信息响应Schema @@ -40,7 +41,7 @@ export const CurrentUserSchema = t.Object({ examples: ['https://example.com/avatar.jpg', null] }), /** 手机号 */ - mobile: t.Union([t.String(), t.Null()], { + phone: t.Union([t.String(), t.Null()], { description: '手机号码', examples: ['13800138000', null] }), @@ -66,5 +67,149 @@ export const CurrentUserSchema = t.Object({ }) }); +/** + * 用户列表查询参数Schema + * @description 用户列表查询的请求参数验证规则 + */ +export const UserListQuerySchema = createQuerySchema(t.Object({ + // 用户特有参数 + keyword: t.Optional(t.String({ + minLength: 1, + maxLength: 100, + description: '搜索关键词,支持用户名、邮箱模糊搜索', + examples: ['admin', 'test@example.com'] + })), + status: t.Optional(t.Union([ + t.Literal('active'), + t.Literal('inactive'), + t.Literal('pending') + ], { + description: '用户状态筛选', + examples: ['active', 'inactive', 'pending'] + })), + gender: t.Optional(t.Union([ + t.Literal(0), + t.Literal(1), + t.Literal(2), + t.Literal('0'), + t.Literal('1'), + t.Literal('2'), + ], { + description: '性别筛选:0-未知,1-男,2-女', + examples: [0, 1, 2] + })), + isRoot: t.Optional(t.Boolean({ + description: '是否超级管理员筛选', + examples: [true, false] + })) +})); + +/** + * 用户列表项Schema + * @description 用户列表中单个用户的数据结构 + */ +export const UserListItemSchema = t.Object({ + /** 用户ID */ + id: t.String({ + description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', + examples: ['1', '2', '3'] + }), + /** 用户名 */ + username: t.String({ + description: '用户名', + examples: ['admin', 'testuser'] + }), + /** 邮箱地址 */ + email: t.String({ + description: '邮箱地址', + examples: ['user@example.com'] + }), + /** 手机号 */ + mobile: t.Union([t.String(), t.Null()], { + description: '手机号码', + examples: ['13800138000', null] + }), + /** 昵称 */ + nickname: t.Union([t.String(), t.Null()], { + description: '用户昵称', + examples: ['管理员', '测试用户', null] + }), + /** 头像URL */ + avatar: t.Union([t.String(), t.Null()], { + description: '用户头像URL', + examples: ['https://example.com/avatar.jpg', null] + }), + /** 账号状态 */ + status: t.String({ + description: '账号状态', + examples: ['active', 'inactive', 'pending'] + }), + /** 性别 */ + gender: t.Union([t.Number(), t.Null()], { + description: '性别:0-未知,1-男,2-女', + examples: [0, 1, 2, null] + }), + /** 生日 */ + birthday: t.Union([t.String(), t.Null()], { + description: '生日', + examples: ['1990-01-01', null] + }), + /** 个人简介 */ + bio: t.Union([t.String(), t.Null()], { + description: '个人简介', + examples: ['这是一段个人简介', null] + }), + /** 登录次数 */ + loginCount: t.Number({ + description: '登录次数', + examples: [0, 10, 100] + }), + /** 最后登录时间 */ + lastLoginAt: t.Union([t.String(), t.Null()], { + description: '最后登录时间', + examples: ['2024-12-19T10:30:00Z', null] + }), + /** 最后登录IP */ + lastLoginIp: t.Union([t.String(), t.Null()], { + description: '最后登录IP', + examples: ['192.168.1.1', null] + }), + /** 失败尝试次数 */ + failedAttempts: t.Number({ + description: '失败尝试次数', + examples: [0, 1, 5] + }), + /** 是否超级管理员 */ + isRoot: t.Boolean({ + description: '是否超级管理员', + examples: [true, false] + }), + /** 创建时间 */ + createdAt: t.String({ + description: '创建时间', + examples: ['2024-12-19T10:30:00Z'] + }), + /** 更新时间 */ + updatedAt: t.String({ + description: '更新时间', + examples: ['2024-12-19T10:30:00Z'] + }) +}); + +/** + * 用户列表响应Schema + * @description 用户列表查询的响应数据结构 + */ +export const UserListResponseSchema = createPaginationResponseSchema(UserListItemSchema); + /** 当前用户信息响应类型 */ -export type CurrentUserResponse = Static; \ No newline at end of file +export type CurrentUserResponse = Static; + +/** 用户列表查询参数类型 */ +export type UserListQueryRequest = Static; + +/** 用户列表项类型 */ +export type UserListItem = Static; + +/** 用户列表响应类型 */ +export type UserListResponse = Static; \ No newline at end of file diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 7d01fba..d5c0d3b 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -4,15 +4,18 @@ * @date 2024-12-19 * @lastEditor AI Assistant * @lastEditTime 2025-01-07 - * @description 用户模块的业务逻辑实现,包括获取当前用户信息等 + * @description 用户模块的业务逻辑实现,包括获取当前用户信息、用户列表查询等 */ import { Logger } from '@/plugins/logger/logger.service'; import { db } from '@/plugins/drizzle/drizzle.service'; import { sysUsers } from '@/eneities'; -import { eq } from 'drizzle-orm'; +import { eq, like, and, desc, asc, sql } from 'drizzle-orm'; import { successResponse, errorResponse, BusinessError } from '@/utils/responseFormate'; -import type { GetCurrentUserSuccessType } from './user.response'; + +import { calculatePagination, normalizePaginationParams } from '@/utils/pagination'; +import type { GetCurrentUserSuccessType, GetUserListSuccessType } from './user.response'; +import type { UserListQueryRequest, UserListItem } from './user.schema'; /** * 用户服务类 @@ -59,18 +62,133 @@ export class UserService { Logger.info(`获取用户信息成功:${userId} - ${userData.username}`); return successResponse({ - id: userData.id!.toString(), + id: userId, // 使用传入的字符串ID,避免精度丢失 username: userData.username, email: userData.email, nickname: userData.nickname, avatar: userData.avatar, - mobile: userData.mobile, + phone: userData.mobile, status: userData.status, lastLoginAt: userData.lastLoginAt || null, createdAt: userData.createdAt, updatedAt: userData.updatedAt }, '获取用户信息成功'); } + + /** + * 获取用户列表 + * @param query 查询参数 + * @returns Promise + * @throws BusinessError 业务逻辑错误 + * @type API ===================================================================== + */ + public async getUserList(query: UserListQueryRequest): Promise { + // 标准化分页参数 + const { page, pageSize, sortBy, sortOrder } = normalizePaginationParams(query); + const { keyword, status, gender, isRoot } = query; + + // 构建查询条件 + const conditions = []; + + // 关键词搜索(用户名、邮箱模糊搜索) + if (keyword) { + conditions.push( + sql`(${sysUsers.username} LIKE ${`%${keyword}%`} OR ${sysUsers.email} LIKE ${`%${keyword}%`})` + ); + } + + // 状态筛选 + if (status) { + conditions.push(eq(sysUsers.status, status)); + } + + // 性别筛选 + if (gender !== undefined) { + conditions.push(eq(sysUsers.gender, gender)); + } + + // 超级管理员筛选 + if (isRoot !== undefined) { + conditions.push(eq(sysUsers.isRoot, isRoot ? 1 : 0)); + } + + // 只查询未删除的用户 + conditions.push(sql`${sysUsers.deletedAt} IS NULL`); + + // 构建排序 + const orderBy = sortBy === 'username' ? sysUsers.username : + sortBy === 'email' ? sysUsers.email : + sortBy === 'updatedAt' ? sysUsers.updatedAt : + sysUsers.createdAt; + + const orderDirection = sortOrder === 'asc' ? asc : desc; + + // 查询总数 + const countResult = await db() + .select({ count: sql`count(${sysUsers.id})` }) + .from(sysUsers) + .where(and(...conditions)); + + const total = Number(countResult[0]?.count || 0); + + // 查询数据 + const users = await db() + .select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + mobile: sysUsers.mobile, + nickname: sysUsers.nickname, + avatar: sysUsers.avatar, + status: sysUsers.status, + gender: sysUsers.gender, + birthday: sysUsers.birthday, + bio: sysUsers.bio, + loginCount: sysUsers.loginCount, + lastLoginAt: sysUsers.lastLoginAt, + lastLoginIp: sysUsers.lastLoginIp, + failedAttempts: sysUsers.failedAttempts, + isRoot: sysUsers.isRoot, + createdAt: sysUsers.createdAt, + updatedAt: sysUsers.updatedAt + }) + .from(sysUsers) + .where(and(...conditions)) + .orderBy(orderDirection(orderBy)) + .limit(pageSize) + .offset((page - 1) * pageSize); + + // 转换数据格式 + const userList: UserListItem[] = users.map(user => ({ + id: user.id!.toString(), // 确保ID以字符串形式返回 + username: user.username, + email: user.email, + mobile: user.mobile, + nickname: user.nickname, + avatar: user.avatar, + status: user.status, + gender: user.gender, + birthday: user.birthday, + bio: user.bio, + loginCount: user.loginCount, + lastLoginAt: user.lastLoginAt || null, + lastLoginIp: user.lastLoginIp, + failedAttempts: user.failedAttempts, + isRoot: user.isRoot === 1, + createdAt: user.createdAt, + updatedAt: user.updatedAt + })); + + // 计算分页信息 + const pagination = calculatePagination(total, page, pageSize); + + Logger.info(`获取用户列表成功:总数${total},当前页${page},每页${pageSize}条`); + + return successResponse({ + ...pagination, + data: userList + }, '获取用户列表成功'); + } } // 导出单例实例 diff --git a/src/utils/pagination.ts b/src/utils/pagination.ts new file mode 100644 index 0000000..f8363af --- /dev/null +++ b/src/utils/pagination.ts @@ -0,0 +1,129 @@ +/** + * @file 分页工具 + * @author AI Assistant + * @date 2024-12-19 + * @lastEditor AI Assistant + * @lastEditTime 2025-01-07 + * @description 分页查询相关的Schema和工具函数 + */ + +import { t, type Static, type TSchema } from 'elysia'; + +/** + * 基础分页查询参数Schema + */ +export const BasePaginationSchema = t.Object({ + /** 页码,从1开始 */ + page: t.Optional(t.Number({ + minimum: 1, + description: '页码,从1开始', + examples: [1, 2, 3], + default: 1 + })), + /** 每页大小,最大100 */ + pageSize: t.Optional(t.Number({ + minimum: 1, + maximum: 100, + description: '每页大小,最大100', + examples: [10, 20, 50], + default: 20 + })), + /** 排序字段 */ + sortBy: t.Optional(t.String({ + description: '排序字段', + examples: ['createdAt', 'updatedAt', 'username', 'email'], + default: 'createdAt' + })), + /** 排序方向 */ + sortOrder: t.Optional(t.Union([ + t.Literal('asc'), + t.Literal('desc') + ], { + description: '排序方向', + examples: ['asc', 'desc'], + default: 'desc' + })) +}); + +/** + * 创建带分页的查询Schema + * @description 将分页参数和自定义参数合并为一个Schema,避免t.Intersect的问题 + * @param customSchema 自定义参数Schema + * @returns 合并后的Schema + */ +export const createQuerySchema = (customSchema: any) => { + return t.Object({ + ...BasePaginationSchema.properties, + ...customSchema.properties + }); +} + +/** + * 基础分页查询参数类型 + */ +export type BasePaginationRequest = Static; + +/** + * 分页响应数据Schema + * @param dataSchema 数据项的Schema + */ +export const createPaginationResponseSchema = (dataSchema: T) => { + return t.Object({ + /** 总记录数 */ + total: t.Number({ + description: '总记录数', + examples: [100, 250, 1000] + }), + /** 当前页码 */ + page: t.Number({ + description: '当前页码', + examples: [1, 2, 3] + }), + /** 每页大小 */ + pageSize: t.Number({ + description: '每页大小', + examples: [10, 20, 50] + }), + /** 数据列表 */ + data: t.Array(dataSchema) + }); +}; + +/** + * 分页响应数据类型 + */ +export type PaginationResponse = { + total: number; + page: number; + pageSize: number; + data: T[]; +}; + +/** + * 计算分页信息 + * @param total 总记录数 + * @param page 当前页码 + * @param pageSize 每页大小 + * @returns 分页信息 + */ +export const calculatePagination = (total: number, page: number, pageSize: number) => { + return { + total, + page, + pageSize + }; +}; + +/** + * 验证和标准化分页参数 + * @param params 原始参数 + * @returns 标准化后的参数 + */ +export const normalizePaginationParams = (params: Partial): Required => { + return { + page: Math.max(1, params.page || 1), + pageSize: Math.min(100, Math.max(1, params.pageSize || 20)), + sortBy: params.sortBy || 'createdAt', + sortOrder: params.sortOrder || 'desc' + }; +}; \ No newline at end of file diff --git a/src/utils/schema.ts b/src/utils/schema.ts new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/utils/schema.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tasks/M2-基础用户系统-开发任务计划.md b/tasks/M2-基础用户系统-开发任务计划.md index 91c7720..8270ed4 100644 --- a/tasks/M2-基础用户系统-开发任务计划.md +++ b/tasks/M2-基础用户系统-开发任务计划.md @@ -124,21 +124,21 @@ ### 👤 用户管理模块 (User Module) - P0优先级 -- [ ] 9.0 GET /users/me - 获取当前用户信息接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 9.1 扩展user.schema.ts - 定义当前用户Schema - - [ ] 9.2 扩展user.response.ts - 定义当前用户响应格式 - - [ ] 9.3 扩展user.service.ts - 实现当前用户业务逻辑 - - [ ] 9.4 更新user.controller.ts - 实现当前用户路由 - - [ ] 9.5 创建user.test.md - 编写当前用户测试用例文档 +- [x] 9.0 GET /users/me - 获取当前用户信息接口 + - [x] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 + - [x] 9.1 扩展user.schema.ts - 定义当前用户Schema + - [x] 9.2 扩展user.response.ts - 定义当前用户响应格式 + - [x] 9.3 扩展user.service.ts - 实现当前用户业务逻辑 + - [x] 9.4 更新user.controller.ts - 实现当前用户路由 + - [x] 9.5 创建user.test.md - 编写当前用户测试用例文档 -- [ ] 10.0 GET /users - 用户列表查询接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 10.1 扩展user.schema.ts - 定义用户列表Schema - - [ ] 10.2 扩展user.response.ts - 定义用户列表响应格式 - - [ ] 10.3 扩展user.service.ts - 实现用户列表业务逻辑 - - [ ] 10.4 扩展user.controller.ts - 实现用户列表路由 - - [ ] 10.5 扩展user.test.md - 编写用户列表测试用例文档 +- [x] 10.0 GET /users - 用户列表查询接口 + - [x] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 + - [x] 10.1 扩展user.schema.ts - 定义用户列表Schema + - [x] 10.2 扩展user.response.ts - 定义用户列表响应格式 + - [x] 10.3 扩展user.service.ts - 实现用户列表业务逻辑 + - [x] 10.4 扩展user.controller.ts - 实现用户列表路由 + - [x] 10.5 扩展user.test.md - 编写用户列表测试用例文档 - [ ] 11.0 POST /users - 创建用户接口 - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步