diff --git a/.cursor/rules/elysia-rules.mdc b/.cursor/rules/elysia-rules.mdc index ee7b398..17cdb71 100644 --- a/.cursor/rules/elysia-rules.mdc +++ b/.cursor/rules/elysia-rules.mdc @@ -142,12 +142,6 @@ src/utils/ └── response.helper.ts # 响应格式工具 (新增) ``` -- 验证器 (validators/) -``` -src/validators/ -└── global.response.ts # 全局响应格式验证 -``` - - 测试文件 (tests/) ``` src/tests/ diff --git a/src/demo/jwt.ts b/src/demo/jwt.ts deleted file mode 100644 index ec8df94..0000000 --- a/src/demo/jwt.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getTokenConfig } from '@/config'; -import { TOKEN_TYPES } from '@/type/jwt.type'; -import { jwt } from '@elysiajs/jwt'; - - - - -const config = getTokenConfig(TOKEN_TYPES.ACTIVATION) - -const token = jwt().sign() - - - - - - - - diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 44d4ecc..be8f2f1 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -9,7 +9,7 @@ import { Elysia } from 'elysia'; import { RegisterSchema, ActivateSchema, LoginSchema } from './auth.schema'; -import { RegisterResponses, ActivateResponses, LoginResponses } from './auth.response'; +import { RegisterResponsesSchema, ActivateResponsesSchema, LoginResponsesSchema } from './auth.response'; import { authService } from './auth.service'; import { tags } from '@/modules/tags'; @@ -36,7 +36,7 @@ export const authController = new Elysia() tags: [tags.auth], operationId: 'registerUser', }, - response: RegisterResponses, + response: RegisterResponsesSchema, } ) @@ -58,7 +58,7 @@ export const authController = new Elysia() tags: [tags.auth], operationId: 'activateUser', }, - response: ActivateResponses, + response: ActivateResponsesSchema, } ) @@ -80,6 +80,6 @@ export const authController = new Elysia() tags: [tags.auth], operationId: 'loginUser', }, - response: LoginResponses, + response: LoginResponsesSchema, } ); \ No newline at end of file diff --git a/src/modules/auth/auth.response.ts b/src/modules/auth/auth.response.ts index 0a52fa5..6cfbb31 100644 --- a/src/modules/auth/auth.response.ts +++ b/src/modules/auth/auth.response.ts @@ -8,234 +8,54 @@ */ import { t, type Static } from 'elysia'; -import { globalResponseWrapperSchema } from '@/validators/global.response'; +import { responseWrapperSchema } from '@/utils/responseFormate'; -/** - * 用户注册成功响应数据Schema - * @description 用户注册成功后返回的用户信息 - */ -export const RegisterSuccessDataSchema = 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'] - }), - /** 账号状态 */ - status: t.String({ - description: '账号状态', - examples: ['pending', 'active'] - }), - /** 创建时间 */ - createdAt: t.String({ - description: '创建时间', - examples: ['2024-12-19T10:30:00Z'] - }) -}); - -/** - * 用户注册成功响应Schema - * @description 用户注册成功的完整响应格式 - */ -export const RegisterSuccessResponseSchema = globalResponseWrapperSchema(RegisterSuccessDataSchema); - -/** - * 用户注册失败响应Schema - * @description 用户注册失败的错误响应格式 - */ -export const RegisterErrorResponseSchema = t.Object({ - /** 错误代码 */ - code: t.Union([ - t.Literal('VALIDATION_ERROR'), - t.Literal('USERNAME_EXISTS'), - t.Literal('EMAIL_EXISTS'), - t.Literal('CAPTCHA_ERROR'), - t.Literal('INTERNAL_ERROR') - ], { - description: '错误代码', - examples: ['VALIDATION_ERROR', 'USERNAME_EXISTS', 'EMAIL_EXISTS'] - }), - /** 错误信息 */ - message: t.String({ - description: '错误信息', - examples: ['用户名已存在', '邮箱已被注册', '验证码错误'] - }), - /** 错误数据 */ - data: t.Null({ - description: '错误时数据为null' - }) -}); +// ========== 邮箱注册相关响应格式 ========== /** * 用户注册接口响应组合 * @description 用于Controller中定义所有可能的响应格式 */ -export const RegisterResponses = { - 200: RegisterSuccessResponseSchema, - 400: RegisterErrorResponseSchema, - 500: t.Object({ - code: t.Literal('INTERNAL_ERROR'), - message: t.String({ - description: '内部服务器错误', - examples: ['服务器内部错误'] +export const RegisterResponsesSchema = { + 200: responseWrapperSchema(t.Object({ + /** 用户ID */ + id: t.String({ + description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', + examples: ['1', '2', '3'] }), - data: t.Null() - }) + /** 用户名 */ + username: t.String({ + description: '用户名', + examples: ['admin', 'testuser'] + }), + /** 邮箱地址 */ + email: t.String({ + description: '邮箱地址', + examples: ['user@example.com'] + }), + /** 账号状态 */ + status: t.String({ + description: '账号状态', + examples: ['pending', 'active'] + }), + /** 创建时间 */ + createdAt: t.String({ + description: '创建时间', + examples: ['2024-12-19T10:30:00Z'] + }) + })), }; - -// ========== TypeScript类型导出 ========== - /** 用户注册成功响应数据类型 */ -export type RegisterSuccessData = Static; - -/** 用户注册成功响应类型 */ -export type RegisterSuccessResponse = Static; - -/** 用户注册失败响应类型 */ -export type RegisterErrorResponse = Static; +export type RegisterResponsesType = Static; // ========== 邮箱激活相关响应格式 ========== -/** - * 邮箱激活成功响应数据Schema - * @description 邮箱激活成功后返回的用户信息 - */ -export const ActivateSuccessDataSchema = 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'] - }), - /** 账号状态 */ - status: t.String({ - description: '账号状态', - examples: ['active'] - }), - /** 激活时间 */ - updatedAt: t.String({ - description: '激活时间', - examples: ['2024-12-19T10:30:00Z'] - }), - /** 激活成功标识 */ - activated: t.Boolean({ - description: '是否已激活', - examples: [true] - }) -}); - -/** - * 邮箱激活成功响应Schema - * @description 邮箱激活成功的完整响应格式 - */ -export const ActivateSuccessResponseSchema = globalResponseWrapperSchema(ActivateSuccessDataSchema); - -/** - * 邮箱激活失败响应Schema - * @description 邮箱激活失败的错误响应格式 - */ -export const ActivateErrorResponseSchema = t.Object({ - /** 错误代码 */ - code: t.Union([ - t.Literal('INVALID_ACTIVATION_TOKEN'), - t.Literal('ALREADY_ACTIVATED'), - t.Literal('USER_NOT_FOUND'), - t.Literal('TOKEN_EXPIRED'), - t.Literal('INTERNAL_ERROR') - ], { - description: '错误代码', - examples: ['INVALID_ACTIVATION_TOKEN', 'ALREADY_ACTIVATED', 'USER_NOT_FOUND'] - }), - /** 错误信息 */ - message: t.String({ - description: '错误信息', - examples: ['激活令牌无效或已过期', '账号已经激活', '用户不存在'] - }), - /** 错误数据 */ - data: t.Null({ - description: '错误时数据为null' - }) -}); - /** * 邮箱激活接口响应组合 * @description 用于Controller中定义所有可能的响应格式 */ -export const ActivateResponses = { - 200: ActivateSuccessResponseSchema, - 400: ActivateErrorResponseSchema, - 401: t.Object({ - code: t.Literal('UNAUTHORIZED'), - message: t.String({ - description: '认证失败', - examples: ['Token无效', 'Token已过期'] - }), - data: t.Null() - }), - 404: t.Object({ - code: t.Literal('USER_NOT_FOUND'), - message: t.String({ - description: '用户不存在', - examples: ['用户不存在'] - }), - data: t.Null() - }), - 409: t.Object({ - code: t.Literal('ALREADY_ACTIVATED'), - message: t.String({ - description: '账号已激活', - examples: ['账号已经激活'] - }), - data: t.Null() - }), - 500: t.Object({ - code: t.Literal('INTERNAL_ERROR'), - message: t.String({ - description: '内部服务器错误', - examples: ['服务器内部错误'] - }), - data: t.Null() - }) -}; - -// ========== TypeScript类型导出 ========== - -/** 邮箱激活成功响应数据类型 */ -export type ActivateSuccessData = Static; - -/** 邮箱激活成功响应类型 */ -export type ActivateSuccessResponse = Static; - -/** 邮箱激活失败响应类型 */ -export type ActivateErrorResponse = Static; - -// ========== 用户登录相关响应格式 ========== - -/** - * 用户登录成功响应数据Schema - * @description 用户登录成功后返回的用户信息和认证令牌 - */ -export const LoginSuccessDataSchema = t.Object({ - /** 用户基本信息 */ - user: t.Object({ +export const ActivateResponsesSchema = { + 200: responseWrapperSchema(t.Object({ /** 用户ID */ id: t.String({ description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', @@ -256,156 +76,88 @@ export const LoginSuccessDataSchema = t.Object({ description: '账号状态', examples: ['active'] }), - /** 最后登录时间 */ - lastLoginAt: t.Union([t.String(), t.Null()], { - description: '最后登录时间', - examples: ['2024-12-19T10:30:00Z', null] + /** 激活时间 */ + updatedAt: t.String({ + description: '激活时间', + examples: ['2024-12-19T10:30:00Z'] + }), + /** 激活成功标识 */ + activated: t.Boolean({ + description: '是否已激活', + examples: [true] }) - }), - /** 认证令牌信息 */ - tokens: t.Object({ - /** 访问令牌 */ - accessToken: t.String({ - description: 'JWT访问令牌', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] - }), - /** 刷新令牌 */ - refreshToken: t.String({ - description: 'JWT刷新令牌', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] - }), - /** 令牌类型 */ - tokenType: t.String({ - description: '令牌类型', - examples: ['Bearer'] - }), - /** 过期时间(秒) */ - expiresIn: t.String({ - description: '访问令牌过期时间(秒)', - examples: [7200, 86400] - }), - /** 刷新令牌过期时间(秒) */ - refreshExpiresIn: t.String({ - description: '刷新令牌过期时间(秒)', - examples: [2592000] - }) - }) -}); + })), +}; -/** - * 用户登录成功响应Schema - * @description 用户登录成功的完整响应格式 - */ -export const LoginSuccessResponseSchema = globalResponseWrapperSchema(LoginSuccessDataSchema); +/** 邮箱激活成功响应数据类型 */ +export type ActivateSuccessType = Static; -/** - * 用户登录失败响应Schema - * @description 用户登录失败的错误响应格式 - */ -export const LoginErrorResponseSchema = t.Object({ - /** 错误代码 */ - code: t.Union([ - t.Literal('VALIDATION_ERROR'), - t.Literal('USER_NOT_FOUND'), - t.Literal('INVALID_PASSWORD'), - t.Literal('ACCOUNT_NOT_ACTIVATED'), - t.Literal('ACCOUNT_LOCKED'), - t.Literal('TOO_MANY_ATTEMPTS'), - t.Literal('CAPTCHA_REQUIRED'), - t.Literal('CAPTCHA_ERROR'), - t.Literal('INTERNAL_ERROR') - ], { - description: '错误代码', - examples: ['USER_NOT_FOUND', 'INVALID_PASSWORD', 'ACCOUNT_NOT_ACTIVATED'] - }), - /** 错误信息 */ - message: t.String({ - description: '错误信息', - examples: ['用户不存在', '密码错误', '账号未激活'] - }), - /** 错误数据 */ - data: t.Union([ - t.Null(), - t.Object({ - /** 登录失败次数 */ - attempts: t.Optional(t.Number({ - description: '登录失败次数', - examples: [3, 5] - })), - /** 账号锁定时间 */ - lockUntil: t.Optional(t.String({ - description: '账号锁定到期时间', - examples: ['2024-12-19T11:30:00Z'] - })), - /** 是否需要验证码 */ - captchaRequired: t.Optional(t.Boolean({ - description: '是否需要验证码', - examples: [true] - })) - }) - ], { - description: '错误时的附加数据' - }) -}); +// ========== 用户登录相关响应格式 ========== /** * 用户登录接口响应组合 * @description 用于Controller中定义所有可能的响应格式 */ -export const LoginResponses = { - 200: LoginSuccessResponseSchema, - 400: LoginErrorResponseSchema, - 401: t.Object({ - code: t.Literal('UNAUTHORIZED'), - message: t.String({ - description: '认证失败', - examples: ['用户名或密码错误', '账号未激活'] +export const LoginResponsesSchema = { + 200: responseWrapperSchema(t.Object({ + /** 用户基本信息 */ + user: 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'] + }), + /** 账号状态 */ + status: t.String({ + description: '账号状态', + examples: ['active'] + }), + /** 最后登录时间 */ + lastLoginAt: t.Union([t.String(), t.Null()], { + description: '最后登录时间', + examples: ['2024-12-19T10:30:00Z', null] + }) }), - data: t.Null() - }), - 423: t.Object({ - code: t.Literal('ACCOUNT_LOCKED'), - message: t.String({ - description: '账号被锁定', - examples: ['账号已被锁定,请稍后再试'] - }), - data: t.Object({ - lockUntil: t.String({ - description: '锁定到期时间', - examples: ['2024-12-19T11:30:00Z'] + /** 认证令牌信息 */ + tokens: t.Object({ + /** 访问令牌 */ + accessToken: t.String({ + description: 'JWT访问令牌', + examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] + }), + /** 刷新令牌 */ + refreshToken: t.String({ + description: 'JWT刷新令牌', + examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] + }), + /** 令牌类型 */ + tokenType: t.String({ + description: '令牌类型', + examples: ['Bearer'] + }), + /** 过期时间(秒) */ + expiresIn: t.String({ + description: '访问令牌过期时间(秒)', + examples: [7200, 86400] + }), + /** 刷新令牌过期时间(秒) */ + refreshExpiresIn: t.String({ + description: '刷新令牌过期时间(秒)', + examples: [2592000] }) }) - }), - 429: t.Object({ - code: t.Literal('TOO_MANY_ATTEMPTS'), - message: t.String({ - description: '登录次数过多', - examples: ['登录次数过多,请稍后再试'] - }), - data: t.Object({ - retryAfter: t.Number({ - description: '重试间隔(秒)', - examples: [300, 600] - }) - }) - }), - 500: t.Object({ - code: t.Literal('INTERNAL_ERROR'), - message: t.String({ - description: '内部服务器错误', - examples: ['服务器内部错误'] - }), - data: t.Null() - }) + })), }; -// ========== TypeScript类型导出 ========== - /** 用户登录成功响应数据类型 */ -export type LoginSuccessData = Static; - -/** 用户登录成功响应类型 */ -export type LoginSuccessResponse = Static; - -/** 用户登录失败响应类型 */ -export type LoginErrorResponse = Static; \ No newline at end of file +export type LoginSuccessType = Static; \ No newline at end of file diff --git a/src/modules/auth/auth.schema.ts b/src/modules/auth/auth.schema.ts index f328501..34ae6bf 100644 --- a/src/modules/auth/auth.schema.ts +++ b/src/modules/auth/auth.schema.ts @@ -19,14 +19,14 @@ export const RegisterSchema = t.Object({ minLength: 2, maxLength: 50, description: '用户名,2-50字符', - examples: ['admin', 'testuser'] + examples: ['root', 'testuser'] }), /** 邮箱地址,对应sys_users.email */ email: t.String({ format: 'email', maxLength: 100, description: '邮箱地址', - examples: ['user@example.com'] + examples: ['x71291@outlook.com'] }), /** 密码,6-50字符 */ password: t.String({ @@ -45,7 +45,7 @@ export const RegisterSchema = t.Object({ /** 验证码会话ID */ captchaId: t.String({ description: '验证码会话ID', - examples: ['uuid-string-here'] + examples: ['cap'] }) }); @@ -59,7 +59,7 @@ export const ActivateSchema = t.Object({ minLength: 10, maxLength: 1000, description: '邮箱激活令牌,JWT格式,24小时有效', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] + examples: ['eyJhbGciOiJIUzI1NiI'] }) }); @@ -68,19 +68,12 @@ export const ActivateSchema = t.Object({ * @description 用户登录请求参数验证规则 */ export const LoginSchema = t.Object({ - /** 登录标识符,支持用户名或邮箱 */ + /** 用户名/邮箱地址,2-50字符,对应sys_users.username */ identifier: t.String({ minLength: 2, maxLength: 100, - description: '登录标识符,支持用户名或邮箱', - examples: ['admin', 'user@example.com'] - }), - /** 密码 */ - password: t.String({ - minLength: 6, - maxLength: 50, - description: '用户密码', - examples: ['password123'] + description: '用户名/邮箱地址,100字符', + examples: ['root', 'testuser', 'x71291@outlook.com'] }), /** 图形验证码(可选) */ captcha: t.Optional(t.String({ @@ -89,10 +82,17 @@ export const LoginSchema = t.Object({ description: '图形验证码,登录失败次数过多时需要', examples: ['a1b2'] })), + /** 密码,6-50字符 */ + password: t.String({ + minLength: 6, + maxLength: 50, + description: '密码,6-50字符', + examples: ['password123'] + }), /** 验证码会话ID(可选) */ captchaId: t.Optional(t.String({ description: '验证码会话ID,与captcha配对使用', - examples: ['uuid-string-here'] + examples: ['cap'] })), /** 是否记住登录状态 */ rememberMe: t.Optional(t.Boolean({ diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 45c3bc1..14799a3 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -8,25 +8,17 @@ */ import bcrypt from 'bcrypt'; -import { eq } from 'drizzle-orm'; +import { eq, sql } from 'drizzle-orm'; import { db } from '@/plugins/drizzle/drizzle.service'; import { sysUsers } from '@/eneities'; import { captchaService } from '@/modules/captcha/captcha.service'; import { Logger } from '@/plugins/logger/logger.service'; -import { ERROR_CODES } from '@/constants/error-codes'; -import { successResponse, errorResponse, BusinessError } from '@/utils/response.helper'; import { nextId } from '@/utils/snowflake'; import { jwtService } from '@/plugins/jwt/jwt.service'; import { emailService } from '@/plugins/email/email.service'; import type { RegisterRequest, ActivateRequest, LoginRequest } from './auth.schema'; -import type { - RegisterSuccessResponse, - RegisterErrorResponse, - ActivateSuccessResponse, - ActivateErrorResponse, - LoginSuccessResponse, - LoginErrorResponse -} from './auth.response'; +import { successResponse, errorResponse, BusinessError } from '@/utils/responseFormate'; +import type { ActivateSuccessType, LoginSuccessType, RegisterResponsesType } from './auth.response'; /** * 认证服务类 @@ -41,141 +33,102 @@ export class AuthService { * @param request 用户注册请求参数 * @returns Promise */ - async register(request: RegisterRequest): Promise { - try { - Logger.info(`用户注册请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`); - - const { username, email, password, captcha, captchaId } = request; - - // 1. 验证验证码 - await this.validateCaptcha(captcha, captchaId); - - // 2. 检查用户名是否已存在 - await this.checkUsernameExists(username); - - // 3. 检查邮箱是否已存在 - await this.checkEmailExists(email); - - // 4. 密码加密 - const passwordHash = await this.hashPassword(password); - - // 5. 创建用户记录 - const newUser = await this.createUser({ - username, - email, - passwordHash - }); - - // 6. 发送激活邮件 - await this.sendActivationEmail(newUser.id, newUser.email, newUser.username); - - Logger.info(`用户注册成功:${newUser.id} - ${newUser.username}`); - - return successResponse({ - id: newUser.id, - username: newUser.username, - email: newUser.email, - status: newUser.status, - createdAt: newUser.createdAt - }, '用户注册成功,请查收激活邮件'); - - } catch (error) { - Logger.error(new Error(`用户注册失败:${error}`)); - - if (error instanceof BusinessError) { - throw error; - } - - throw new BusinessError('注册失败,请稍后重试', ERROR_CODES.INTERNAL_ERROR); - } + public async register(request: RegisterRequest): Promise { + Logger.info(`用户注册请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`); + + const { username, email, password, captcha, captchaId } = request; + + // 1. 验证验证码 + await this.validateCaptcha(captcha, captchaId); + + // 2. 检查用户名是否已存在 + await this.checkUsernameExists(username); + + // 3. 检查邮箱是否已存在 + await this.checkEmailExists(email); + + // 4. 密码加密 + const passwordHash = await this.hashPassword(password); + + // 5. 创建用户记录 + const newUser = await this.createUser({ + username, + email, + passwordHash + }); + + // 6. 发送激活邮件 + await this.sendActivationEmail(newUser.id, newUser.email, newUser.username); + + Logger.info(`用户注册成功:${newUser.id} - ${newUser.username}`); + + return successResponse({ + id: newUser.id, + username: newUser.username, + email: newUser.email, + status: newUser.status, + createdAt: newUser.createdAt + }, '用户注册成功,请查收激活邮件'); } - + /** * 验证验证码 * @param captcha 验证码 * @param captchaId 验证码ID */ private async validateCaptcha(captcha: string, captchaId: string): Promise { - try { - const result = await captchaService.verifyCaptcha({ - captchaId, - captchaCode: captcha - }); - - if (!result.data?.valid) { - throw new BusinessError( - result.data?.message || '验证码验证失败', - ERROR_CODES.CAPTCHA_ERROR - ); - } - } catch (error) { - if (error instanceof BusinessError) { - throw error; - } - throw new BusinessError('验证码验证失败', ERROR_CODES.CAPTCHA_ERROR); + const result = await captchaService.verifyCaptcha({ + captchaId, + captchaCode: captcha + }); + + if (!result.data?.valid) { + throw new BusinessError( + result.data?.message || '验证码验证失败', + 400 + ); } } - + /** * 检查用户名是否已存在 * @param username 用户名 */ private async checkUsernameExists(username: string): Promise { - try { - const existingUser = await db().select({ id: sysUsers.id }) - .from(sysUsers) - .where(eq(sysUsers.username, username)) - .limit(1); - - if (existingUser.length > 0) { - throw new BusinessError('用户名已存在', ERROR_CODES.USERNAME_EXISTS); - } - } catch (error) { - if (error instanceof BusinessError) { - throw error; - } - Logger.error(new Error(`检查用户名失败:${error}`)); - throw new BusinessError('用户名检查失败', ERROR_CODES.INTERNAL_ERROR); + const existingUser = await db().select({ id: sysUsers.id }) + .from(sysUsers) + .where(eq(sysUsers.username, username)) + .limit(1); + + if (existingUser.length > 0) { + throw new BusinessError('用户名已存在', 400); } } - + /** * 检查邮箱是否已存在 * @param email 邮箱地址 */ private async checkEmailExists(email: string): Promise { - try { - const existingUser = await db().select({ id: sysUsers.id }) - .from(sysUsers) - .where(eq(sysUsers.email, email)) - .limit(1); - - if (existingUser.length > 0) { - throw new BusinessError('邮箱已被注册', ERROR_CODES.EMAIL_EXISTS); - } - } catch (error) { - if (error instanceof BusinessError) { - throw error; - } - Logger.error(new Error(`检查邮箱失败:${error}`)); - throw new BusinessError('邮箱检查失败', ERROR_CODES.INTERNAL_ERROR); + const existingUser = await db().select({ id: sysUsers.id }) + .from(sysUsers) + .where(eq(sysUsers.email, email)) + .limit(1); + + if (existingUser.length > 0) { + throw new BusinessError('邮箱已被注册', 400); } } - + /** * 密码加密 * @param password 原始密码 * @returns Promise 加密后的密码哈希 */ private async hashPassword(password: string): Promise { - try { - return await bcrypt.hash(password, this.BCRYPT_ROUNDS); - } catch (error) { - Logger.error(new Error(`密码加密失败:${error}`)); - throw new BusinessError('密码加密失败', ERROR_CODES.INTERNAL_ERROR); - } + return await bcrypt.hash(password, this.BCRYPT_ROUNDS); } - + /** * 创建用户记录 * @param userData 用户数据 @@ -192,50 +145,44 @@ export class AuthService { status: string; createdAt: string; }> { - try { - const { username, email, passwordHash } = userData; - - const userId = nextId(); // 保持 bigint 类型,避免精度丢失 - Logger.info(`生成用户ID: ${userId.toString()}`); - - const [insertResult] = await db().insert(sysUsers).values({ - id: userId, - username, - email, - passwordHash, - status: 'pending' // 新注册用户状态为待激活 - }); - - // 查询刚创建的用户信息 - const [newUser] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status, - createdAt: sysUsers.createdAt - }) + const { username, email, passwordHash } = userData; + + const userId = nextId(); // 保持 bigint 类型,避免精度丢失 + Logger.info(`生成用户ID: ${userId.toString()}`); + + await db().insert(sysUsers).values({ + id: userId, + username, + email, + passwordHash, + status: 'pending' // 新注册用户状态为待激活 + }); + + // 查询刚创建的用户信息 + const [newUser] = await db().select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + createdAt: sysUsers.createdAt + }) .from(sysUsers) .where(eq(sysUsers.id, userId)) .limit(1); - Logger.info(`创建用户成功: ${JSON.stringify(newUser, null, 2)}`); - - if (!newUser) { - throw new Error('创建用户后查询失败'); - } - - // 确保ID以字符串形式返回,避免精度丢失 - return { - id: userId.toString(), // 直接使用原始的 bigint userId,转换为字符串 - username: newUser.username, - email: newUser.email, - status: newUser.status, - createdAt: newUser.createdAt - }; - } catch (error) { - console.log(error); - Logger.error(error as Error); - throw new BusinessError('创建用户失败', ERROR_CODES.INTERNAL_ERROR); - } + Logger.info(`创建用户成功: ${JSON.stringify(newUser, null, 2)}`); + + // if (!newUser) { + // throw new BusinessError('创建用户后查询失败', 500); + // } + + // 确保ID以字符串形式返回,避免精度丢失 + return { + id: userId!.toString(), // 直接使用原始的 bigint userId,转换为字符串 + username: newUser!.username, + email: newUser!.email, + status: newUser!.status, + createdAt: newUser!.createdAt + }; } /** @@ -243,95 +190,44 @@ export class AuthService { * @param request 邮箱激活请求参数 * @returns Promise */ - async activate(request: ActivateRequest): Promise { - try { - Logger.info(`邮箱激活请求开始处理`); - - const { token } = request; - - // 1. 验证激活Token - const tokenPayload = await this.validateActivationToken(token); - - // 2. 根据Token中的用户ID查询用户 - const user = await this.getUserById(tokenPayload.userId); - - // 3. 检查用户是否已经激活 - if (user.status === 'active') { - throw new BusinessError('账号已经激活', ERROR_CODES.ALREADY_ACTIVATED); - } - - // 4. 更新用户状态为激活 - const activatedUser = await this.updateUserStatus(tokenPayload.userId, 'active'); - - // 5. 发送激活成功邮件(可选) - await this.sendActivationSuccessEmail(activatedUser.email, activatedUser.username); - - Logger.info(`用户邮箱激活成功:${activatedUser.id} - ${activatedUser.email}`); - - return successResponse({ - id: activatedUser.id, - username: activatedUser.username, - email: activatedUser.email, - status: activatedUser.status, - updatedAt: activatedUser.updatedAt, - activated: true - }, '邮箱激活成功'); - - } catch (error) { - Logger.error(new Error(`邮箱激活失败:${error}`)); - - if (error instanceof BusinessError) { - throw error; - } - - throw new BusinessError('激活失败,请稍后重试', ERROR_CODES.INTERNAL_ERROR); - } - } + public async activate(request: ActivateRequest): Promise { + Logger.info(`邮箱激活请求开始处理`); - /** - * 验证激活Token - * @param token 激活Token - * @returns Promise 激活Token载荷 - */ - private async validateActivationToken(token: string): Promise { - try { - // 注意:这里需要在controller中使用jwt.verify进行实际验证 - // 这里提供业务逻辑验证 - - // 基础格式验证 - if (!token || token.length < 10) { - throw new BusinessError('激活令牌格式无效', ERROR_CODES.INVALID_ACTIVATION_TOKEN); - } - - // 模拟token解析(实际应该在controller中用jwt.verify) - let payload: any; - try { - // 这里应该是jwt.verify(token)的结果 - payload = JSON.parse(token); // 临时实现,实际应该从controller传入已验证的载荷 - } catch (parseError) { - throw new BusinessError('激活令牌解析失败', ERROR_CODES.INVALID_ACTIVATION_TOKEN); - } - - // 验证token载荷格式 - if (!jwtService.verifyActivationTokenPayload(payload)) { - throw new BusinessError('激活令牌载荷无效', ERROR_CODES.INVALID_ACTIVATION_TOKEN); - } - - // 检查token是否过期(如果有exp字段) - if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) { - throw new BusinessError('激活令牌已过期', ERROR_CODES.INVALID_ACTIVATION_TOKEN); - } - - Logger.info(`激活Token验证成功,用户ID: ${payload.userId}`); - return payload; - - } catch (error) { - if (error instanceof BusinessError) { - throw error; - } - Logger.error(new Error(`激活Token验证失败:${error}`)); - throw new BusinessError('激活令牌验证失败', ERROR_CODES.INVALID_ACTIVATION_TOKEN); + const { token } = request; + + // 1. 验证激活Token + const tokenPayload = await jwtService.verifyToken(token); + + Logger.info(tokenPayload); + + if (tokenPayload?.error) { + throw new BusinessError('激活令牌验证失败', 400); } + + // 2. 根据Token中的用户ID查询用户 + const user = await this.getUserById(tokenPayload.userId); + + // 3. 检查用户是否已经激活 + if (user.status === 'active') { + throw new BusinessError('账号已经激活', 400); + } + + // 4. 更新用户状态为激活 + const activatedUser = await this.updateUserStatus(tokenPayload.userId, 'active'); + + // 5. 发送激活成功邮件(可选) + this.sendActivationSuccessEmail(activatedUser.email, activatedUser.username); + + Logger.info(`用户邮箱激活成功:${activatedUser.id} - ${activatedUser.email}`); + + return successResponse({ + id: activatedUser.id, + username: activatedUser.username, + email: activatedUser.email, + status: activatedUser.status, + updatedAt: activatedUser.updatedAt, + activated: true + }, '邮箱激活成功'); } /** @@ -347,39 +243,30 @@ export class AuthService { createdAt: string; updatedAt: string; }> { - try { - const [user] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status, - createdAt: sysUsers.createdAt, - updatedAt: sysUsers.updatedAt - }) + const [user] = await db().select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + createdAt: sysUsers.createdAt, + updatedAt: sysUsers.updatedAt + }) .from(sysUsers) .where(eq(sysUsers.id, BigInt(userId))) .limit(1); - - if (!user) { - throw new BusinessError('用户不存在', ERROR_CODES.USER_NOT_FOUND); - } - - return { - id: userId, // 使用传入的字符串ID,避免精度丢失 - username: user.username, - email: user.email, - status: user.status, - createdAt: user.createdAt, - updatedAt: user.updatedAt - }; - - } catch (error) { - if (error instanceof BusinessError) { - throw error; - } - Logger.error(new Error(`获取用户信息失败:${error}`)); - throw new BusinessError('获取用户信息失败', ERROR_CODES.INTERNAL_ERROR); + + if (!user) { + throw new BusinessError('用户不存在', 400); } + + return { + id: userId, // 使用传入的字符串ID,避免精度丢失 + username: user.username, + email: user.email, + status: user.status, + createdAt: user.createdAt, + updatedAt: user.updatedAt + }; } /** @@ -395,42 +282,32 @@ export class AuthService { status: string; updatedAt: string; }> { - try { - // 更新用户状态 - await db().update(sysUsers) - .set({ - status: status,}) - .where(eq(sysUsers.id, BigInt(userId))); - - // 查询更新后的用户信息 - const [updatedUser] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status, - updatedAt: sysUsers.updatedAt + // 更新用户状态 + await db().update(sysUsers) + .set({ + status: status, }) + .where(eq(sysUsers.id, BigInt(userId))); + + // 查询更新后的用户信息 + const [updatedUser] = await db().select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + updatedAt: sysUsers.updatedAt + }) .from(sysUsers) .where(eq(sysUsers.id, BigInt(userId))) .limit(1); - - if (!updatedUser) { - throw new Error('用户状态更新后查询失败'); - } - - return { - id: userId, // 使用传入的字符串ID,避免精度丢失 - username: updatedUser.username, - email: updatedUser.email, - status: updatedUser.status, - updatedAt: updatedUser.updatedAt - }; - - } catch (error) { - console.log(error); - Logger.error(error as Error); - throw new BusinessError('更新用户状态失败', ERROR_CODES.INTERNAL_ERROR); - } + + return { + id: userId, // 使用传入的字符串ID,避免精度丢失 + username: updatedUser!.username, + email: updatedUser!.email, + status: updatedUser!.status, + updatedAt: updatedUser!.updatedAt + }; } /** @@ -438,61 +315,48 @@ export class AuthService { * @param request 用户登录请求参数 * @returns Promise */ - async login(request: LoginRequest): Promise { - try { - Logger.info(`用户登录请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`); - - const { identifier, password, captcha, captchaId, rememberMe = false } = request; - - // 1. 验证验证码(如果提供) - if (captcha && captchaId) { - // await this.validateCaptcha(captcha, captchaId); - } - - // 2. 查找用户(支持用户名或邮箱) - const user = await this.findUserByIdentifier(identifier); + async login(request: LoginRequest): Promise { + Logger.info(`用户登录请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`); - // todo 判断帐号状态,是否锁定 - - // 3. 验证密码 - await this.verifyPassword(password, user.passwordHash); - - // 4. 检查账号状态 - await this.checkAccountStatus(user); - - // 5. 生成JWT令牌 - const tokens = jwtService.generateTokens(user, rememberMe); - - // 6. 更新最后登录时间 - await this.updateLastLoginTime(user.id); - - // 7. 记录登录日志 - await this.recordLoginLog(user.id, identifier); - - Logger.info(`用户登录成功:${user.id} - ${user.username}`); + const { identifier, password, captcha, captchaId, rememberMe = false } = request; - console.log(tokens); - - return successResponse({ - user: { - id: user.id, - username: user.username, - email: user.email, - status: user.status, - lastLoginAt: user.lastLoginAt - }, - tokens - }, '登录成功'); - - } catch (error) { - Logger.error(new Error(`用户登录失败:${error}`)); - - if (error instanceof BusinessError) { - throw error; - } - - throw new BusinessError('登录失败,请稍后重试', ERROR_CODES.INTERNAL_ERROR); + // 1. 验证验证码(如果提供) + if (captcha && captchaId) { + await this.validateCaptcha(captcha, captchaId); } + + // 2. 查找用户(支持用户名或邮箱) + const user = await this.findUserByIdentifier(identifier); + + // todo 判断帐号状态,是否锁定 + + // 3. 验证密码 + await this.verifyPassword(password, user.passwordHash); + + // 4. 检查账号状态 + await this.checkAccountStatus(user); + + // 5. 生成JWT令牌 + const tokens = jwtService.generateTokens(user, rememberMe); + + // 6. 更新最后登录时间 + await this.updateLastLoginTime(user.id); + + // 7. 记录登录日志 + await this.recordLoginLog(user.id, identifier); + + Logger.info(`用户登录成功:${user.id} - ${user.username}`); + + return successResponse({ + user: { + id: user.id, + username: user.username, + email: user.email, + status: user.status, + lastLoginAt: user.lastLoginAt + }, + tokens + }, '登录成功'); } /** @@ -508,47 +372,38 @@ export class AuthService { passwordHash: string; lastLoginAt: string | null; }> { - try { - // 判断是否为邮箱格式 - const isEmail = identifier.includes('@'); - - // 构建查询条件 - const whereCondition = isEmail - ? eq(sysUsers.email, identifier) - : eq(sysUsers.username, identifier); - - const [user] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status, - passwordHash: sysUsers.passwordHash, - lastLoginAt: sysUsers.lastLoginAt - }) + // 判断是否为邮箱格式 + const isEmail = identifier.includes('@'); + + // 构建查询条件 + const whereCondition = isEmail + ? eq(sysUsers.email, identifier) + : eq(sysUsers.username, identifier); + + const [user] = await db().select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + passwordHash: sysUsers.passwordHash, + lastLoginAt: sysUsers.lastLoginAt + }) .from(sysUsers) .where(whereCondition) .limit(1); - - if (!user) { - throw new BusinessError('用户不存在', ERROR_CODES.USER_NOT_FOUND); - } - - return { - id: user.id.toString(), // 转换为字符串避免精度丢失 - username: user.username, - email: user.email, - status: user.status, - passwordHash: user.passwordHash, - lastLoginAt: user.lastLoginAt - }; - - } catch (error) { - if (error instanceof BusinessError) { - throw error; - } - Logger.error(new Error(`查找用户失败:${error}`)); - throw new BusinessError('查找用户失败', ERROR_CODES.INTERNAL_ERROR); + + if (!user) { + throw new BusinessError('用户不存在', 400); } + + return { + id: user.id!.toString(), // 转换为字符串避免精度丢失 + username: user.username, + email: user.email, + status: user.status, + passwordHash: user.passwordHash, + lastLoginAt: user.lastLoginAt + }; } /** @@ -557,20 +412,11 @@ export class AuthService { * @param passwordHash 密码哈希 */ private async verifyPassword(password: string, passwordHash: string): Promise { - try { - const isValid = await bcrypt.compare(password, passwordHash); - - if (!isValid) { - // todo 记录错误登录次数,如果超过5次,则锁定账号 - throw new BusinessError('密码错误', ERROR_CODES.INVALID_PASSWORD); - } - - } catch (error) { - if (error instanceof BusinessError) { - throw error; - } - Logger.error(new Error(`密码验证失败:${error}`)); - throw new BusinessError('密码验证失败', ERROR_CODES.INTERNAL_ERROR); + const isValid = await bcrypt.compare(password, passwordHash); + + if (!isValid) { + // todo 记录错误登录次数,如果超过5次,则锁定账号 + throw new BusinessError('密码错误', 400); } } @@ -578,17 +424,17 @@ export class AuthService { * 检查账号状态 * @param user 用户信息 */ - private async checkAccountStatus(user: { status: string }): Promise { + private checkAccountStatus(user: { status: string }) { if (user.status === 'pending') { - throw new BusinessError('账号未激活,请先激活账号', ERROR_CODES.ACCOUNT_NOT_ACTIVATED); + throw new BusinessError('账号未激活,请先激活账号', 400); } - + if (user.status === 'locked') { - throw new BusinessError('账号已被锁定,请联系管理员', ERROR_CODES.ACCOUNT_LOCKED); + throw new BusinessError('账号已被锁定,请联系管理员', 400); } - + if (user.status !== 'active') { - throw new BusinessError('账号状态异常,请联系管理员', ERROR_CODES.ACCOUNT_LOCKED); + throw new BusinessError('账号状态异常,请联系管理员', 400); } } @@ -597,15 +443,12 @@ export class AuthService { * @param userId 用户ID */ private async updateLastLoginTime(userId: string): Promise { - try { - await db().update(sysUsers) - .set({ lastLoginAt: new Date().toISOString() }) - .where(eq(sysUsers.id, BigInt(userId))); - - } catch (error) { - // 记录错误但不影响登录流程 - Logger.error(new Error(`更新最后登录时间失败:${error}`)); - } + await db().update(sysUsers) + .set({ + lastLoginAt: sql`NOW()`, // 使用 MySQL 的 NOW() 函数 + loginCount: sql`${sysUsers.loginCount} + 1` + }) + .where(eq(sysUsers.id, BigInt(userId))); } /** @@ -616,7 +459,7 @@ export class AuthService { private async recordLoginLog(userId: string, identifier: string): Promise { try { Logger.info(`用户登录日志:用户ID=${userId}, 标识符=${identifier}, 时间=${new Date().toISOString()}`); - + // TODO: 如果有登录日志表,可以在这里记录到数据库 // await db().insert(loginLogs).values({ // userId: BigInt(userId), @@ -625,7 +468,7 @@ export class AuthService { // ip: '0.0.0.0', // 从请求中获取 // userAgent: 'unknown' // 从请求中获取 // }); - + } catch (error) { // 记录错误但不影响登录流程 Logger.error(new Error(`记录登录日志失败:${error}`)); @@ -649,9 +492,8 @@ export class AuthService {

感谢您的使用!

` }); - - Logger.info(`激活成功邮件发送成功:${email}`); - + // Logger.info(`激活成功邮件发送成功:${email}`); + } catch (error) { // 邮件发送失败不影响激活流程,只记录日志 Logger.warn(`激活成功邮件发送失败:${email}, 错误:${error}`); @@ -668,8 +510,8 @@ export class AuthService { try { // 生成激活Token载荷 const activationTokenPayload = await jwtService.generateActivationToken(userId, email, username); - - Logger.debug({activationTokenPayload}); + + Logger.debug({ activationTokenPayload }); // 发送激活邮件 await emailService.sendEmail({ to: email, @@ -682,9 +524,8 @@ export class AuthService {

如果您没有注册,请忽略此邮件。

` }); - + Logger.info(`激活邮件发送成功:${email}`); - } catch (error) { // 邮件发送失败不影响注册流程,只记录日志 Logger.warn(`激活邮件发送失败:${email}, 错误:${error}`); diff --git a/src/modules/captcha/captcha.controller.ts b/src/modules/captcha/captcha.controller.ts index e5595f6..1a54c72 100644 --- a/src/modules/captcha/captcha.controller.ts +++ b/src/modules/captcha/captcha.controller.ts @@ -7,7 +7,7 @@ import { Elysia, t } from 'elysia'; import { GenerateCaptchaSchema, VerifyCaptchaSchema } from './captcha.schema'; -import { GenerateCaptchaResponses, VerifyCaptchaResponses } from './captcha.response'; +import { responseWrapperSchema } from '@/utils/responseFormate'; import { captchaService } from './captcha.service'; import { tags } from '@/modules/tags'; @@ -26,7 +26,7 @@ export const captchaController = new Elysia() description: '生成图形验证码,支持自定义尺寸和过期时间', tags: [tags.captcha], }, - response: GenerateCaptchaResponses, + response: {200: responseWrapperSchema(t.Any())}, } ) @@ -44,7 +44,7 @@ export const captchaController = new Elysia() description: '验证用户输入的验证码是否正确', tags: [tags.captcha], }, - response: VerifyCaptchaResponses, + response: {200: responseWrapperSchema(t.Any())}, } ) @@ -54,33 +54,13 @@ export const captchaController = new Elysia() */ .post( '/cleanup', - async () => { - const cleanedCount = await captchaService.cleanupExpiredCaptchas(); - return { - code: 'SUCCESS' as const, - message: '清理完成', - data: { cleanedCount } - }; - }, + () => captchaService.cleanupExpiredCaptchas(), { detail: { summary: '清理过期验证码', description: '清理Redis中已过期的验证码数据', tags: [tags.captcha], }, - response: { - 200: t.Object({ - code: t.Literal('SUCCESS'), - message: t.String(), - data: t.Object({ - cleanedCount: t.Number() - }) - }), - 500: t.Object({ - code: t.Literal('INTERNAL_ERROR'), - message: t.String(), - data: t.Null(), - }), - }, + response: {200: responseWrapperSchema(t.Any())}, } ); \ No newline at end of file diff --git a/src/modules/captcha/captcha.response.ts b/src/modules/captcha/captcha.response.ts deleted file mode 100644 index cd977d4..0000000 --- a/src/modules/captcha/captcha.response.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @file 验证码模块响应格式定义 - * @author AI助手 - * @date 2024-12-27 - * @description 验证码相关接口的响应格式定义 - */ - -import { t, type Static } from 'elysia'; -import { globalResponseWrapperSchema } from '@/validators/global.response'; -import { CaptchaGenerateResponseSchema } from './captcha.schema'; - -/** - * 生成验证码成功响应 - */ -export const GenerateCaptchaSuccessResponseSchema = globalResponseWrapperSchema(CaptchaGenerateResponseSchema); -export type GenerateCaptchaSuccessResponse = Static; - -/** - * 验证验证码成功响应 - */ -export const VerifyCaptchaSuccessResponseSchema = globalResponseWrapperSchema(t.Object({ - valid: t.Boolean({ description: '验证结果' }), - message: t.String({ description: '验证消息' }) -})); -export type VerifyCaptchaSuccessResponse = Static; - -/** - * 验证码不存在错误响应 - */ -export const CaptchaNotFoundResponseSchema = t.Object({ - code: t.Literal('CAPTCHA_NOT_FOUND'), - message: t.String({ examples: ['验证码不存在或已过期'] }), - data: t.Null(), -}); -export type CaptchaNotFoundResponse = Static; - -/** - * 验证码错误响应 - */ -export const CaptchaInvalidResponseSchema = t.Object({ - code: t.Literal('CAPTCHA_INVALID'), - message: t.String({ examples: ['验证码错误'] }), - data: t.Null(), -}); -export type CaptchaInvalidResponse = Static; - -/** - * 验证码过期错误响应 - */ -export const CaptchaExpiredResponseSchema = t.Object({ - code: t.Literal('CAPTCHA_EXPIRED'), - message: t.String({ examples: ['验证码已过期'] }), - data: t.Null(), -}); -export type CaptchaExpiredResponse = Static; - -/** - * 生成验证码接口响应组合 - */ -export const GenerateCaptchaResponses = { - 200: GenerateCaptchaSuccessResponseSchema, - 400: t.Object({ - code: t.Literal('VALIDATION_ERROR'), - message: t.String(), - data: t.Null(), - }), - 500: t.Object({ - code: t.Literal('INTERNAL_ERROR'), - message: t.String(), - data: t.Null(), - }), -}; - -/** - * 验证验证码接口响应组合 - */ -export const VerifyCaptchaResponses = { - 200: VerifyCaptchaSuccessResponseSchema, - 400: t.Union([ - CaptchaNotFoundResponseSchema, - CaptchaInvalidResponseSchema, - CaptchaExpiredResponseSchema, - t.Object({ - code: t.Literal('VALIDATION_ERROR'), - message: t.String(), - data: t.Null(), - }) - ]), - 500: t.Object({ - code: t.Literal('INTERNAL_ERROR'), - message: t.String(), - data: t.Null(), - }), -}; \ No newline at end of file diff --git a/src/modules/captcha/captcha.service.ts b/src/modules/captcha/captcha.service.ts index 7bfd16e..79c55c8 100644 --- a/src/modules/captcha/captcha.service.ts +++ b/src/modules/captcha/captcha.service.ts @@ -7,20 +7,15 @@ import { randomBytes, randomInt } from 'crypto'; import { createCanvas } from 'canvas'; -import type { - GenerateCaptchaRequest, - VerifyCaptchaRequest, - CaptchaData, - CaptchaGenerateResponse +import type { + GenerateCaptchaRequest, + VerifyCaptchaRequest, + CaptchaData, + CaptchaGenerateResponse } from './captcha.schema'; -import type { - GenerateCaptchaSuccessResponse, - VerifyCaptchaSuccessResponse -} from './captcha.response'; import { Logger } from '@/plugins/logger/logger.service'; import { redisService } from '@/plugins/redis/redis.service'; -import { ERROR_CODES } from '@/constants/error-codes'; -import { successResponse } from '@/utils/response.helper'; +import { successResponse, errorResponse, BusinessError } from '@/utils/responseFormate'; export class CaptchaService { /** @@ -28,63 +23,56 @@ export class CaptchaService { * @param request 生成验证码请求参数 * @returns Promise */ - async generateCaptcha(request: GenerateCaptchaRequest): Promise { - try { - Logger.info(`生成验证码请求:${JSON.stringify(request)}`); - - const { - type = 'image', - width = 200, - height = 60, - length = 4, - expireTime = 300 - } = request; + async generateCaptcha(body: GenerateCaptchaRequest) { + const { + type = 'image', + width = 200, + height = 60, + length = 4, + expireTime = 300 + } = body; - // 生成验证码ID - const captchaId = `captcha_${randomBytes(16).toString('hex')}`; - - // 生成验证码内容 - const code = this.generateRandomCode(length); - - // 计算过期时间 - const expireTimestamp = Date.now() + (expireTime * 1000); - - let imageData: string | undefined; - - if (type === 'image') { - // 生成图形验证码 - imageData = await this.generateImageCaptcha(code, width, height); - } - - // 构建验证码数据 - const captchaData: CaptchaData = { - id: captchaId, - code: code.toLowerCase(), // 存储时转为小写,验证时忽略大小写 - type, - image: imageData, - expireTime: expireTimestamp, - createdAt: Date.now() - }; - - // 存储到Redis - const redisKey = `captcha:${captchaId}`; - await redisService.setex(redisKey, expireTime, JSON.stringify(captchaData)); - - Logger.info(`验证码生成成功:${captchaId}`); - - // 构建响应数据 - const responseData: CaptchaGenerateResponse = { - id: captchaId, - image: imageData || '', - expireTime: expireTimestamp, - type - }; - - return successResponse(responseData, '验证码生成成功'); - } catch (error) { - Logger.error(new Error(`生成验证码失败:${error}`)); - throw error; + // 生成验证码ID + const captchaId = `captcha_${randomBytes(16).toString('hex')}`; + + // 生成验证码内容 + const code = this.generateRandomCode(length); + + // 计算过期时间 + const expireTimestamp = Date.now() + (expireTime * 1000); + + let imageData: string | undefined; + + if (type === 'image') { + // 生成图形验证码 + imageData = await this.generateImageCaptcha(code, width, height); } + + // 构建验证码数据 + const captchaData: CaptchaData = { + id: captchaId, + code: code.toLowerCase(), // 存储时转为小写,验证时忽略大小写 + type, + image: imageData, + expireTime: expireTimestamp, + createdAt: Date.now() + }; + + // 存储到Redis + const redisKey = `captcha:${captchaId}`; + await redisService.setex(redisKey, expireTime, JSON.stringify(captchaData)); + + Logger.info(`验证码生成成功:${captchaId} ${code}`); + + // 构建响应数据 + const responseData: CaptchaGenerateResponse = { + id: captchaId, + image: imageData || '', + expireTime: expireTimestamp, + type + }; + + return successResponse(responseData); } /** @@ -92,58 +80,37 @@ export class CaptchaService { * @param request 验证验证码请求参数 * @returns Promise */ - async verifyCaptcha(request: VerifyCaptchaRequest): Promise { - try { - Logger.info(`验证验证码请求:${JSON.stringify(request)}`); - - const { captchaId, captchaCode, scene } = request; - - // 从Redis获取验证码数据 - const redisKey = `captcha:${captchaId}`; - const captchaDataStr = await redisService.get(redisKey); - - if (!captchaDataStr) { - Logger.warn(`验证码不存在:${captchaId}`); - return successResponse( - { valid: false, message: '验证码不存在或已过期' }, - '验证失败' - ); - } - - const captchaData: CaptchaData = JSON.parse(captchaDataStr); - - // 检查是否过期 - if (Date.now() > captchaData.expireTime) { - Logger.warn(`验证码已过期:${captchaId}`); - // 删除过期的验证码 - await redisService.del(redisKey); - return successResponse( - { valid: false, message: '验证码已过期' }, - '验证失败' - ); - } - - // 验证验证码内容(忽略大小写) - const isValid = captchaData.code.toLowerCase() === captchaCode.toLowerCase(); - - if (isValid) { - // 验证成功后删除验证码,防止重复使用 - await redisService.del(redisKey); - Logger.info(`验证码验证成功:${captchaId}`); - return successResponse( - { valid: true, message: '验证码验证成功' }, - '验证成功' - ); - } else { - Logger.warn(`验证码错误:${captchaId},输入:${captchaCode},正确:${captchaData.code}`); - return successResponse( - { valid: false, message: '验证码错误' }, - '验证失败' - ); - } - } catch (error) { - Logger.error(new Error(`验证验证码失败:${error}`)); - throw error; + async verifyCaptcha(request: VerifyCaptchaRequest) { + const { captchaId, captchaCode, scene } = request; + + // 从Redis获取验证码数据 + const redisKey = `captcha:${captchaId}`; + const captchaDataStr = await redisService.get(redisKey); + + if (!captchaDataStr) { + throw new BusinessError('验证码不存在或已过期', 400); + } + + const captchaData: CaptchaData = JSON.parse(captchaDataStr); + + // 检查是否过期 + if (Date.now() > captchaData.expireTime) { + await redisService.del(redisKey); + throw new BusinessError('验证码已过期:', 400); + } + + // 验证验证码内容(忽略大小写) + const isValid = captchaData.code.toLowerCase() === captchaCode.toLowerCase(); + + if (isValid) { + // 验证成功后删除验证码,防止重复使用 + await redisService.del(redisKey); + Logger.info(`验证码验证成功:${captchaId}`); + return successResponse( + { valid: true }, '验证码验证成功' + ); + } else { + throw new BusinessError('验证码错误', 400); } } @@ -173,11 +140,11 @@ export class CaptchaService { try { const canvas = createCanvas(width, height); const ctx = canvas.getContext('2d'); - + // 设置背景色 ctx.fillStyle = '#f0f0f0'; ctx.fillRect(0, 0, width, height); - + // 添加干扰线 for (let i = 0; i < 3; i++) { ctx.strokeStyle = `rgb(${randomInt(100, 200)}, ${randomInt(100, 200)}, ${randomInt(100, 200)})`; @@ -187,27 +154,27 @@ export class CaptchaService { ctx.lineTo(randomInt(width), randomInt(height)); ctx.stroke(); } - + // 添加干扰点 for (let i = 0; i < 50; i++) { ctx.fillStyle = `rgb(${randomInt(100, 200)}, ${randomInt(100, 200)}, ${randomInt(100, 200)})`; ctx.fillRect(randomInt(width), randomInt(height), 1, 1); } - + // 绘制验证码文字 const fontSize = Math.min(width / code.length, height * 0.6); ctx.font = `bold ${fontSize}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - + const charWidth = width / code.length; for (let i = 0; i < code.length; i++) { const x = charWidth * i + charWidth / 2; const y = height / 2 + randomInt(-5, 5); - + // 随机颜色 ctx.fillStyle = `rgb(${randomInt(0, 100)}, ${randomInt(0, 100)}, ${randomInt(0, 100)})`; - + // 随机旋转 ctx.save(); ctx.translate(x, y); @@ -215,7 +182,7 @@ export class CaptchaService { ctx.fillText(code[i]!, 0, 0); ctx.restore(); } - + // 转换为Base64 const buffer = canvas.toBuffer('image/png'); const base64 = buffer.toString('base64'); @@ -230,29 +197,26 @@ export class CaptchaService { * 清理过期验证码 * @returns Promise 清理的验证码数量 */ - async cleanupExpiredCaptchas(): Promise { - try { - const pattern = 'captcha:*'; - const keys = await redisService.keys(pattern); - let cleanedCount = 0; - - for (const key of keys) { - const captchaDataStr = await redisService.get(key); - if (captchaDataStr) { - const captchaData: CaptchaData = JSON.parse(captchaDataStr); - if (Date.now() > captchaData.expireTime) { - await redisService.del(key); - cleanedCount++; - } + async cleanupExpiredCaptchas() { + const pattern = 'captcha:*'; + const keys = await redisService.keys(pattern); + let cleanedCount = 0; + + for (const key of keys) { + const captchaDataStr = await redisService.get(key); + if (captchaDataStr) { + const captchaData: CaptchaData = JSON.parse(captchaDataStr); + if (Date.now() > captchaData.expireTime) { + await redisService.del(key); + cleanedCount++; } } - - Logger.info(`清理过期验证码完成,共清理 ${cleanedCount} 个`); - return cleanedCount; - } catch (error) { - Logger.error(new Error(`清理过期验证码失败:${error}`)); - throw error; } + + Logger.info(`清理过期验证码完成,共清理 ${cleanedCount} 个`); + return successResponse( + { cleanedCount }, '清理完成' + ); } } diff --git a/src/modules/example/example.controller.ts b/src/modules/example/example.controller.ts deleted file mode 100644 index ac5df66..0000000 --- a/src/modules/example/example.controller.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @file 样例控制器 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口控制器,演示完整的接口开发流程 - */ - -import { Elysia } from 'elysia'; -import { jwtAuthPlugin } from '@/plugins/jwt/jwt.plugins'; -import { GetUserByUsernameSchema } from './example.schema'; -import { GetUserByUsernameResponses } from './example.response'; -import { tags } from '@/modules/tags'; -import { exampleService } from './example.service'; - -/** - * 样例控制器 - * @description 提供样例接口的路由定义和请求处理逻辑 - * @modification hotok 2025-06-29 实现根据用户名查询用户接口 - */ -export const exampleController = new Elysia() - // 使用JWT认证插件 - .use(jwtAuthPlugin) - /** - * 根据用户名查询用户信息 - * @route GET /api/example/user/:username - * @description 通过用户名查询用户的详细信息,需要JWT认证 - * @param username 用户名,路径参数,长度2-50字符 - * @returns 用户信息对象或错误响应 - * @modification hotok 2025-06-29 初始实现 - */ - .get( - '/user/:username', - ({ params, user }) => { - return exampleService.findUserByUsername({ params, user }); - }, - { - // 路径参数验证 - params: GetUserByUsernameSchema, - - // API文档配置 - detail: { - summary: '根据用户名查询用户信息', - description: - '通过用户名查询用户的详细信息,需要JWT身份认证。返回用户的基本信息,不包含敏感数据如密码。', - tags: [tags.user, tags.example], - security: [{ bearerAuth: [] }], - }, - // 响应格式定义 - response: GetUserByUsernameResponses, - }, - ); diff --git a/src/modules/example/example.response.ts b/src/modules/example/example.response.ts deleted file mode 100644 index c0daa66..0000000 --- a/src/modules/example/example.response.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @file 样例接口响应Schema定义 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口的响应结构定义,基于全局响应格式扩展 - * - * ⚠️ 响应格式管理规范: - * 1. 当前文件只定义成功响应(200)的具体数据结构 - * 2. 错误响应(400, 401, 422, 500等)统一使用 @/validators/global.response.ts 中的 CommonResponses - * 3. 这样可以保证错误响应格式的一致性,便于前端统一处理 - * 4. 如需自定义错误响应,请在 CommonResponses 中添加,而不是在具体业务文件中定义 - */ - -import { t } from 'elysia'; -import { UserInfoSchema } from './example.schema'; -import { CommonResponses } from '@/validators/global.response'; - -/** - * 根据用户名查询用户成功响应 - * @description 定义成功查询用户时返回的数据结构 - */ -export const GetUserByUsernameSuccessResponse = t.Object({ - code: t.Literal(0, { - description: '成功响应码', - }), - message: t.String({ - description: '成功消息', - examples: ['查询用户成功'], - }), - data: UserInfoSchema, -}); - -/** - * 根据用户名查询用户接口的所有可能响应 - * @description 组合成功响应和公共错误响应,确保格式一致性 - */ -export const GetUserByUsernameResponses = { - /** 200 查询成功 - 使用自定义成功响应 */ - 200: GetUserByUsernameSuccessResponse, - - /** 400 业务错误 - 使用全局公共错误响应 */ - 400: CommonResponses[400], - - /** 401 认证失败 - 使用全局公共错误响应 */ - 401: CommonResponses[401], - - /** 422 参数验证失败 - 使用全局公共错误响应 */ - 422: CommonResponses[422], - - /** 500 服务器内部错误 - 使用全局公共错误响应 */ - 500: CommonResponses[500], -}; \ No newline at end of file diff --git a/src/modules/example/example.schema.ts b/src/modules/example/example.schema.ts deleted file mode 100644 index 22e1777..0000000 --- a/src/modules/example/example.schema.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @file 样例接口Schema定义 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口的请求参数和响应数据的TypeBox schema定义 - */ - -import { t, type Static } from 'elysia'; - -/** - * 根据用户名查询用户的请求参数Schema - */ -export const GetUserByUsernameSchema = t.Object({ - /** 用户名,必填,长度2-50字符 */ - username: t.String({ - minLength: 2, - maxLength: 50, - description: '用户名,用于查询用户信息', - examples: ['admin', 'testuser', 'zhangsan'], - }), -}); - -/** - * 用户信息返回数据Schema - */ -export const UserInfoSchema = t.Object({ - /** 用户ID */ - id: t.Number({ - description: '用户唯一标识ID', - examples: [1, 2, 100], - }), - /** 用户名 */ - username: t.String({ - description: '用户名', - examples: ['admin', 'testuser'], - }), - /** 邮箱 */ - email: t.String({ - description: '用户邮箱', - examples: ['admin@example.com', 'user@test.com'], - }), - /** 用户昵称 */ - nickname: t.Optional(t.String({ - description: '用户昵称', - examples: ['管理员', '测试用户'], - })), - /** 用户头像URL */ - avatar: t.Optional(t.String({ - description: '用户头像URL', - examples: ['https://example.com/avatar.jpg'], - })), - /** 用户状态:0-禁用,1-启用 */ - status: t.Number({ - description: '用户状态,0-禁用,1-启用', - examples: [0, 1], - }), - /** 创建时间 */ - createdAt: t.String({ - description: '用户创建时间', - examples: ['2024-06-29T10:30:00.000Z'], - }), - /** 更新时间 */ - updatedAt: t.String({ - description: '用户最后更新时间', - examples: ['2024-06-29T10:30:00.000Z'], - }), -}); - -/** - * 根据用户名查询用户的请求参数类型 - */ -export type GetUserByUsernameType = Static; - -/** - * 用户信息数据类型 - */ -export type UserInfoType = Static; \ No newline at end of file diff --git a/src/modules/example/example.service.ts b/src/modules/example/example.service.ts deleted file mode 100644 index f9d316a..0000000 --- a/src/modules/example/example.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @file 样例业务逻辑服务 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口的业务逻辑实现 - * - * 设计思路: - * 1. 接收用户名参数,对参数进行基础校验 - * 2. 使用Drizzle ORM查询数据库中的用户信息 - * 3. 处理查询结果:用户存在则返回用户信息,不存在则抛出业务异常 - * 4. 对敏感信息进行过滤,不返回密码哈希等敏感字段 - * 5. 统一异常处理,确保返回标准的错误响应格式 - * 6. 记录操作日志,便于系统监控和问题排查 - * - * 安全考虑: - * - 严格过滤敏感信息,不向客户端返回密码哈希 - * - 对查询参数进行SQL注入防护(Drizzle ORM自带防护) - * - 记录查询日志,便于安全审计 - */ - -import { eq } from 'drizzle-orm'; -import { db } from '@/plugins/drizzle/drizzle.service'; -import { sysUsers as users } from '@/eneities'; -import { ERROR_CODES } from '@/validators/global.response'; -import { type GetUserByUsernameType } from './example.schema'; -import type { JwtUserType } from '@/type/jwt.type'; - -/** - * 样例服务类 - * @description 提供用户相关的业务逻辑处理 - */ -export class ExampleService { - async findUserByUsername({ params, user }: { params: GetUserByUsernameType; user: JwtUserType }) { - const { username } = params; - user; - // 使用Drizzle ORM查询用户信息 - const userList = await db() - .select({ - id: users.id, - username: users.username, - email: users.email, - nickname: users.nickname, - avatar: users.avatar, - status: users.status, - createdAt: users.createdAt, - updatedAt: users.updatedAt, - }) - .from(users) - .where(eq(users.username, username)) - .limit(1); - - // 检查查询结果 - if (!userList || userList.length === 0) { - return { - code: 400 as const, - message: '用户不存在', - data: null, - }; - } - - const userInfo = userList[0]!; - - // 返回成功响应 - return { - code: ERROR_CODES.SUCCESS, - message: '查询用户成功', - data: { - id: userInfo.id, - username: userInfo.username, - email: userInfo.email, - nickname: userInfo.nickname || undefined, - avatar: userInfo.avatar || undefined, - status: userInfo.status, - createdAt: userInfo.createdAt.toISOString(), - updatedAt: userInfo.updatedAt.toISOString(), - }, - }; - } -} - -/** - * 导出样例服务实例 - */ -export const exampleService = new ExampleService(); diff --git a/src/modules/example/example.test.ts b/src/modules/example/example.test.ts deleted file mode 100644 index 1be011b..0000000 --- a/src/modules/example/example.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @file 样例接口测试用例 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 样例接口的完整测试用例,覆盖正常、异常、边界场景 - */ - -import { describe, it, expect, beforeAll } from 'vitest'; -import { Elysia } from 'elysia'; -import { exampleController } from './example.controller'; -import { jwtPlugin } from '@/plugins/jwt/jwt.plugins'; - -// 创建测试应用实例 -const app = new Elysia() - .use(jwtPlugin) - .use(exampleController); - -// 测试用的JWT Token(需要根据实际情况生成) -let testToken = ''; - -describe('样例接口测试', () => { - beforeAll(async () => { - // 在实际测试中,这里应该通过登录接口获取有效token - // 这里为了演示,假设我们有一个有效的token - // 创建临时的JWT实例来生成测试token - const tempApp = new Elysia().use(jwtPlugin); - const context = { jwt: tempApp.derive().jwt }; - - testToken = await context.jwt.sign({ - userId: 1, - username: 'admin', - iat: Math.floor(Date.now() / 1000), - }); - }); - - describe('GET /api/example/user/:username', () => { - it('应该成功查询存在的用户', async () => { - const res = await app.fetch( - new Request('http://localhost/example/user/admin', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}`, - }, - }), - ); - - const body = (await res.json()) as any; - console.log('成功查询响应:', body); - - expect(res.status).toBe(200); - expect(body.code).toBe(0); - expect(body.message).toBe('查询用户成功'); - expect(body.data).toBeDefined(); - expect(typeof body.data.id).toBe('number'); - expect(typeof body.data.username).toBe('string'); - expect(typeof body.data.email).toBe('string'); - }); - - it('用户名过短应返回422', async () => { - const res = await app.fetch( - new Request('http://localhost/example/user/a', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}`, - }, - }), - ); - - const body = (await res.json()) as any; - console.log('用户名过短响应:', body); - - expect(res.status).toBe(422); - expect(body.code).toBe(422); - expect(body.message).toMatch(/用户名/); - }); - - it('用户名过长应返回422', async () => { - const res = await app.fetch( - new Request('http://localhost/example/user/' + 'a'.repeat(51), { - method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}`, - }, - }), - ); - - const body = (await res.json()) as any; - console.log('用户名过长响应:', body); - - expect(res.status).toBe(422); - expect(body.code).toBe(422); - expect(body.message).toMatch(/用户名/); - }); - - it('用户不存在应返回400', async () => { - const res = await app.fetch( - new Request('http://localhost/example/user/nonexistentuser12345', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}`, - }, - }), - ); - - const body = (await res.json()) as any; - console.log('用户不存在响应:', body); - - expect(res.status).toBe(400); - expect(body.code).toBe(400); - expect(body.message).toBe('用户不存在'); - expect(body.data).toBeNull(); - }); - - it('缺少Authorization头应返回401', async () => { - const res = await app.fetch( - new Request('http://localhost/example/user/admin', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - console.log('缺少Authorization响应:', body); - - expect(res.status).toBe(401); - expect(body.code).toBe(401); - expect(body.message).toMatch(/Token|认证|授权/); - }); - - it('无效Token应返回401', async () => { - const res = await app.fetch( - new Request('http://localhost/example/user/admin', { - method: 'GET', - headers: { - Authorization: 'Bearer invalid_token_here', - }, - }), - ); - - const body = (await res.json()) as any; - console.log('无效Token响应:', body); - - expect(res.status).toBe(401); - expect(body.code).toBe(401); - expect(body.message).toMatch(/Token|认证|授权/); - }); - - it('错误的Authorization格式应返回401', async () => { - const res = await app.fetch( - new Request('http://localhost/example/user/admin', { - method: 'GET', - headers: { - Authorization: 'InvalidFormat token', - }, - }), - ); - - const body = (await res.json()) as any; - console.log('错误Authorization格式响应:', body); - - expect(res.status).toBe(401); - expect(body.code).toBe(401); - expect(body.message).toMatch(/Token|认证|授权/); - }); - }); - - describe('GET /api/example/health', () => { - it('应该返回模块健康状态', async () => { - const res = await app.fetch( - new Request('http://localhost/example/health', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - console.log('健康检查响应:', body); - - expect(res.status).toBe(200); - expect(body.code).toBe(0); - expect(body.message).toBe('样例模块运行正常'); - expect(body.data).toBeDefined(); - expect(body.data.module).toBe('example'); - expect(body.data.status).toBe('healthy'); - expect(typeof body.data.timestamp).toBe('string'); - }); - }); -}); \ No newline at end of file diff --git a/src/modules/index.ts b/src/modules/index.ts index 4ff03c3..c265915 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -11,7 +11,6 @@ import { Elysia } from 'elysia'; import { healthController } from './health/health.controller'; // import { userController } from './user/user.controller'; import { testController } from './test/test.controller'; -import { exampleController } from './example/example.controller'; import { captchaController } from './captcha/captcha.controller'; import { authController } from './auth/auth.controller'; @@ -33,8 +32,6 @@ export const controllers = new Elysia({ .group('/test', (app) => app.use(testController)) // 健康检查接口 .group('/health', (app) => app.use(healthController)) - // 样例接口 - .group('/example', (app) => app.use(exampleController)) // 认证接口 .group('/auth', (app) => app.use(authController)) // 验证码接口 diff --git a/src/modules/tags.ts b/src/modules/tags.ts index 7b6512e..9f49431 100644 --- a/src/modules/tags.ts +++ b/src/modules/tags.ts @@ -20,8 +20,6 @@ export const tags = { health: 'Health', /** 测试接口 */ test: 'Test', - /** 样例接口 */ - example: 'example', /** 文件上传接口 */ upload: 'Upload', /** 系统管理接口 */ diff --git a/src/plugins/drizzle/drizzle.service.ts b/src/plugins/drizzle/drizzle.service.ts index 56f7cb2..ce26eeb 100644 --- a/src/plugins/drizzle/drizzle.service.ts +++ b/src/plugins/drizzle/drizzle.service.ts @@ -192,7 +192,6 @@ export class DrizzleService { message: 'SQL查询执行', query: query.replace(/\s+/g, ' ').trim(), params: params, - timestamp: new Date().toISOString(), }); }, } : false, diff --git a/src/plugins/email/email.service.ts b/src/plugins/email/email.service.ts index eb70dcf..d029ff8 100644 --- a/src/plugins/email/email.service.ts +++ b/src/plugins/email/email.service.ts @@ -123,7 +123,7 @@ export class EmailService { public async initialize(): Promise { // 防止重复初始化 if (this._isInitialized && this._transporter) { - Logger.info('邮件服务已初始化,返回现有实例'); + Logger.debug('邮件服务已初始化,返回现有实例'); return this._transporter; } @@ -154,7 +154,7 @@ export class EmailService { this._isInitialized = true; this.updateStatus('healthy', 'connected'); - Logger.info({ + Logger.debug({ message: '邮件服务初始化成功', host: smtpConfig.host, port: smtpConfig.port, @@ -226,7 +226,7 @@ export class EmailService { retryCount, }; - Logger.info({ + Logger.debug({ message: '邮件发送成功', messageId: result.messageId, to: options.to, @@ -598,7 +598,7 @@ export class EmailService { this._isInitialized = false; this.updateStatus('unhealthy', 'disconnected'); - Logger.info('邮件服务已关闭'); + Logger.debug('邮件服务已关闭'); } catch (error) { Logger.error(error instanceof Error ? error : new Error('关闭邮件服务时出错')); } diff --git a/src/plugins/errorHandle/errorHandler.plugins.ts b/src/plugins/errorHandle/errorHandler.plugins.ts index 80d5883..6926a4d 100644 --- a/src/plugins/errorHandle/errorHandler.plugins.ts +++ b/src/plugins/errorHandle/errorHandler.plugins.ts @@ -59,6 +59,14 @@ export const errorHandlerPlugin = (app: Elysia) => errors: error.message, }; } + case 400: { + set.status = code; + return { + code: error.code, + message: '参数验证错误', + errors: error.message, + }; + } default: { // 处理 ElysiaCustomStatusResponse status抛出的异常 if (error?.constructor?.name === 'ElysiaCustomStatusResponse') { @@ -70,9 +78,9 @@ export const errorHandlerPlugin = (app: Elysia) => }; } - console.log('error', error); + console.log('error ==================== \n', error, `\n ==================== error \n`, code, 'code =============='); set.status = 500; - Logger.error(error); + Logger.error(error as Error); return { code: 500, message: '服务器内部错误', diff --git a/src/plugins/jwt/jwt.service.ts b/src/plugins/jwt/jwt.service.ts index 7f83cca..ce5f6b4 100644 --- a/src/plugins/jwt/jwt.service.ts +++ b/src/plugins/jwt/jwt.service.ts @@ -7,7 +7,7 @@ import jwt from 'jsonwebtoken'; import { jwtConfig } from '@/config'; -import { TOKEN_TYPES } from '@/type/jwt.type'; +import { TOKEN_TYPES, type JwtPayloadType } from '@/type/jwt.type'; /** * JWT服务类 - 原生版 @@ -78,9 +78,9 @@ export class JwtService { */ verifyToken(token: string) { try { - return jwt.verify(token, jwtConfig.secret) + return jwt.verify(token, jwtConfig.secret) as JwtPayloadType } catch { - return { valid: false }; + return { error: true } as JwtPayloadType; } } } diff --git a/src/plugins/logger/logger.service.ts b/src/plugins/logger/logger.service.ts index 14b033f..b2952af 100644 --- a/src/plugins/logger/logger.service.ts +++ b/src/plugins/logger/logger.service.ts @@ -115,7 +115,7 @@ const formatHTTP = (obj: any): string => { const consoleTransport = new winston.transports.Console({ format: winston.format.combine( - winston.format.timestamp({ format: 'YY/MM/DD HH:mm:ss' }), + winston.format.timestamp({ format: 'YY/MM/DD HH:mm:ss - SSS' }), winston.format.printf(({ timestamp, message, level, stack }) => { // 使用居中对齐格式化日志级别 const levelText = centerText(level.toUpperCase(), 7); @@ -123,14 +123,14 @@ const consoleTransport = new winston.transports.Console({ if (level === 'error' && stack && typeof stack === 'string') { const formattedStack = formatStack(stack); - return `[${chalk.gray(timestamp)}] ${levelFormatted} ${message}\n${formattedStack}`; + return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${message}\n${formattedStack}`; } else if (level === 'error') { - return `[${chalk.gray(timestamp)}] ${levelFormatted} 未定义的异常 ${message}`; + return `[${chalk.red.bold(timestamp)}] ${levelFormatted} 未定义的异常 ${message}`; } else if (level === 'http') { - return `[${chalk.gray(timestamp)}] ${levelFormatted} ${formatHTTP(message)}`; + return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${formatHTTP(message)}`; } - return `[${chalk.gray(timestamp)}] ${levelFormatted} ${formatJSON(message as string, level)}`; + return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${formatJSON(message as string, level)}`; }), ), }); diff --git a/src/plugins/swagger/swagger.plugins.ts b/src/plugins/swagger/swagger.plugins.ts index cb3ea8c..26bb406 100644 --- a/src/plugins/swagger/swagger.plugins.ts +++ b/src/plugins/swagger/swagger.plugins.ts @@ -8,7 +8,6 @@ */ import { swagger } from '@elysiajs/swagger'; -import { ERROR_CODES, ERROR_CODE_DESCRIPTIONS } from '@/validators/global.response'; /** * Swagger插件实例 @@ -117,21 +116,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3O }, }, schemas: { - ErrorCodes: { - type: 'object', - description: '系统错误码定义', - properties: Object.fromEntries( - Object.entries(ERROR_CODES).map(([key, value]) => [ - key, - { - type: 'number', - enum: [value], - description: ERROR_CODE_DESCRIPTIONS[value], - example: value, - }, - ]) - ), - }, BaseResponse: { type: 'object', description: '基础响应结构', diff --git a/src/type/jwt.type.ts b/src/type/jwt.type.ts index d691728..e592799 100644 --- a/src/type/jwt.type.ts +++ b/src/type/jwt.type.ts @@ -60,6 +60,7 @@ export interface JwtPayloadType extends JwtUserType { jti?: string; /** Token生效时间(秒级时间戳) */ nbf?: number; + error?: boolean; } /** diff --git a/src/utils/response.helper.ts b/src/utils/response.helper.ts deleted file mode 100644 index b3558dc..0000000 --- a/src/utils/response.helper.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @file 响应格式工具函数 - * @author AI助手 - * @date 2025-06-29 - * @description 提供统一的响应格式构造函数,确保API响应格式的一致性 - */ - -import type { ErrorCode } from '@/constants/error-codes'; -import { ERROR_CODES, ERROR_CODE_MESSAGES } from '@/constants/error-codes'; - -/** - * 标准API响应格式 - */ -export interface ApiResponse { - /** 业务状态码 */ - code: ErrorCode; - /** 响应消息 */ - message: string; - /** 响应数据 */ - data: T; - /** 时间戳 */ - timestamp?: string; - /** 请求ID(可选,用于追踪) */ - requestId?: string; -} - -/** - * 成功响应构造函数 - * @param data 响应数据 - * @param message 自定义消息,默认使用标准成功消息 - * @returns 标准成功响应格式 - */ -export function successResponse( - data: T, - message: string = ERROR_CODE_MESSAGES[ERROR_CODES.SUCCESS] -): ApiResponse { - return { - code: ERROR_CODES.SUCCESS, - message, - data, - timestamp: new Date().toISOString(), - }; -} - -/** - * 错误响应构造函数 - * @param code 错误码 - * @param message 错误消息,如果不提供则使用默认消息 - * @param data 错误详情数据,默认为null - * @returns 标准错误响应格式 - */ -export function errorResponse( - code: ErrorCode, - message?: string, - data: T = null as T -): ApiResponse { - return { - code, - message: message || ERROR_CODE_MESSAGES[code] || '未知错误', - data, - timestamp: new Date().toISOString(), - }; -} - -/** - * 分页数据响应格式 - */ -export interface PaginatedData { - /** 数据列表 */ - items: T[]; - /** 总记录数 */ - total: number; - /** 当前页码 */ - page: number; - /** 每页记录数 */ - pageSize: number; - /** 总页数 */ - totalPages: number; - /** 是否有下一页 */ - hasNext: boolean; - /** 是否有上一页 */ - hasPrev: boolean; -} - -/** - * 分页响应构造函数 - * @param items 数据列表 - * @param total 总记录数 - * @param page 当前页码 - * @param pageSize 每页记录数 - * @param message 自定义消息 - * @returns 标准分页响应格式 - */ -export function paginatedResponse( - items: T[], - total: number, - page: number, - pageSize: number, - message: string = '查询成功' -): ApiResponse> { - const totalPages = Math.ceil(total / pageSize); - - return successResponse>({ - items, - total, - page, - pageSize, - totalPages, - hasNext: page < totalPages, - hasPrev: page > 1, - }, message); -} - -/** - * 业务错误类 - * @description 用于抛出具有特定错误码的业务异常 - */ -export class BusinessError extends Error { - public readonly code: ErrorCode; - - constructor(message: string, code: ErrorCode = ERROR_CODES.BUSINESS_ERROR) { - super(message); - this.name = 'BusinessError'; - this.code = code; - } -} - -/** - * 验证错误类 - * @description 用于参数验证失败的场景 - */ -export class ValidationError extends BusinessError { - constructor(message: string) { - super(message, ERROR_CODES.VALIDATION_ERROR); - this.name = 'ValidationError'; - } -} - -/** - * 认证错误类 - * @description 用于认证相关的错误 - */ -export class AuthenticationError extends BusinessError { - constructor(message: string, code: ErrorCode = ERROR_CODES.UNAUTHORIZED) { - super(message, code); - this.name = 'AuthenticationError'; - } -} - -/** - * 权限错误类 - * @description 用于权限不足的场景 - */ -export class ForbiddenError extends BusinessError { - constructor(message: string = '权限不足') { - super(message, ERROR_CODES.FORBIDDEN); - this.name = 'ForbiddenError'; - } -} - -/** - * 资源不存在错误类 - * @description 用于资源不存在的场景 - */ -export class NotFoundError extends BusinessError { - constructor(message: string = '资源不存在') { - super(message, ERROR_CODES.NOT_FOUND); - this.name = 'NotFoundError'; - } -} \ No newline at end of file diff --git a/src/utils/responseFormate.ts b/src/utils/responseFormate.ts new file mode 100644 index 0000000..6c42c00 --- /dev/null +++ b/src/utils/responseFormate.ts @@ -0,0 +1,67 @@ +/** + * @file 统一响应格式 + * @author hotok + * @date 2025-07-26 + * @lastEditor hotok + * @lastEditTime 2025-07-26 + * @description 统一响应格式 + */ + +import Logger from "@/plugins/logger/logger.service"; + +/** + * 成功响应 + * @param data 响应数据 + * @param message 响应消息 + * @returns 成功响应 + */ +export const successResponse = (data: any, message: string = 'success') => { + return { + code: 200, + message, + data, + timestamp: new Date().toISOString(), + } +} + +export const errorResponse = (code: number, message: string, type: string, data: any = null) => { + const response = { + code, + message, + data, + type, + timestamp: new Date().toISOString(), + } + Logger.warn(response); + return response +} + +export class BusinessError extends Error { + public readonly code: number; + + constructor(message: string, code: number) { + super(message); + this.name = 'BusinessError'; + this.code = code; + } +} + +import { t } from 'elysia'; + +/** + * 全局响应包装器Schema + * @param dataSchema 数据Schema + * @returns 包装后的响应Schema + */ +export const responseWrapperSchema = (dataSchema: any) => + t.Object({ + code: t.Number({ + description: '响应状态码', + examples: [200, 201], + }), + message: t.String({ + description: '响应消息', + examples: ['操作成功', '操作失败', '创建成功'], + }), + data: dataSchema, + }); \ No newline at end of file diff --git a/src/validators/global.response.ts b/src/validators/global.response.ts deleted file mode 100644 index 9f1d802..0000000 --- a/src/validators/global.response.ts +++ /dev/null @@ -1,310 +0,0 @@ -/** - * @file 全局响应Schema定义 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 定义全局通用的响应结构、错误码说明,供Swagger文档和接口验证使用 - */ - -import { t } from 'elysia'; - -/** - * 全局错误码定义 - * @description 系统错误码说明,便于前端开发和API文档查阅 - */ -export const ERROR_CODES = { - /** 成功 */ - SUCCESS: 0, - /** 通用业务错误 */ - BUSINESS_ERROR: 400, - /** 认证失败 */ - UNAUTHORIZED: 401, - /** 权限不足 */ - FORBIDDEN: 403, - /** 资源未找到 */ - NOT_FOUND: 404, - /** 参数验证失败 */ - VALIDATION_ERROR: 422, - /** 服务器内部错误 */ - INTERNAL_ERROR: 500, - /** 服务不可用 */ - SERVICE_UNAVAILABLE: 503, -} as const; - -/** - * 错误码说明映射 - */ -export const ERROR_CODE_DESCRIPTIONS = { - [ERROR_CODES.SUCCESS]: '操作成功', - [ERROR_CODES.BUSINESS_ERROR]: '业务逻辑错误', - [ERROR_CODES.UNAUTHORIZED]: '身份认证失败,请重新登录', - [ERROR_CODES.FORBIDDEN]: '权限不足,无法访问该资源', - [ERROR_CODES.NOT_FOUND]: '请求的资源不存在', - [ERROR_CODES.VALIDATION_ERROR]: '请求参数验证失败', - [ERROR_CODES.INTERNAL_ERROR]: '服务器内部错误,请稍后重试', - [ERROR_CODES.SERVICE_UNAVAILABLE]: '服务暂时不可用,请稍后重试', -} as const; - -/** - * 基础响应结构Schema - */ -export const BaseResponseSchema = t.Object({ - /** 响应码:0表示成功,其他表示错误 */ - code: t.Number({ - description: '响应码,0表示成功,其他表示错误', - examples: [0, 400, 401, 403, 404, 422, 500, 503], - }), - /** 响应消息 */ - message: t.String({ - description: '响应消息,描述操作结果', - examples: ['操作成功', '参数验证失败', '权限不足'], - }), - /** 响应数据 */ - data: t.Any({ - description: '响应数据,成功时包含具体数据,失败时通常为null', - }), -}); - -/** - * 成功响应Schema - */ -export const SuccessResponseSchema = t.Object({ - code: t.Literal(0, { - description: '成功响应码', - }), - message: t.String({ - description: '成功消息', - examples: ['操作成功', '获取数据成功', '创建成功'], - }), - data: t.Any({ - description: '成功时返回的数据', - }), -}); - -/** - * 错误响应Schema - */ -export const ErrorResponseSchema = t.Object({ - code: t.Number({ - description: '错误响应码', - examples: [400, 401, 403, 404, 422, 500, 503], - }), - message: t.String({ - description: '错误消息', - examples: ['参数验证失败', '认证失败', '权限不足', '资源不存在', '服务器内部错误'], - }), - data: t.Null({ - description: '错误时数据字段为null', - }), -}); - -/** - * 分页响应Schema - */ -export const PaginationResponseSchema = t.Object({ - code: t.Literal(0), - message: t.String(), - data: t.Object({ - /** 分页数据列表 */ - list: t.Array(t.Any(), { - description: '数据列表', - }), - /** 分页信息 */ - pagination: t.Object({ - /** 当前页码 */ - page: t.Number({ - description: '当前页码,从1开始', - minimum: 1, - examples: [1, 2, 3], - }), - /** 每页条数 */ - pageSize: t.Number({ - description: '每页条数', - minimum: 1, - maximum: 100, - examples: [10, 20, 50], - }), - /** 总条数 */ - total: t.Number({ - description: '总条数', - minimum: 0, - examples: [0, 100, 1500], - }), - /** 总页数 */ - totalPages: t.Number({ - description: '总页数', - minimum: 0, - examples: [0, 5, 75], - }), - /** 是否有下一页 */ - hasNext: t.Boolean({ - description: '是否有下一页', - }), - /** 是否有上一页 */ - hasPrev: t.Boolean({ - description: '是否有上一页', - }), - }), - }), -}); - -/** - * 常用HTTP状态码响应模板 - */ -export const CommonResponses = { - /** 200 成功 */ - 200: SuccessResponseSchema, - /** 400 业务错误 */ - 400: ErrorResponseSchema, - /** 401 认证失败 */ - 401: t.Object({ - code: t.Literal(401), - message: t.String({ - examples: ['身份认证失败,请重新登录', 'Token已过期', 'Token格式错误'], - }), - data: t.Null(), - }), - /** 403 权限不足 */ - 403: t.Object({ - code: t.Literal(403), - message: t.String({ - examples: ['权限不足,无法访问该资源', '用户角色权限不够'], - }), - data: t.Null(), - }), - /** 404 资源未找到 */ - 404: t.Object({ - code: t.Literal(404), - message: t.String({ - examples: ['请求的资源不存在', '用户不存在', '文件未找到'], - }), - data: t.Null(), - }), - /** 422 参数验证失败 */ - 422: t.Object({ - code: t.Literal(422), - message: t.String({ - examples: ['请求参数验证失败', '邮箱格式不正确', '密码长度不符合要求'], - }), - data: t.Null(), - }), - /** 500 服务器内部错误 */ - 500: t.Object({ - code: t.Literal(500), - message: t.String({ - examples: ['服务器内部错误,请稍后重试', '数据库连接失败', '系统异常'], - }), - data: t.Null(), - }), - /** 503 服务不可用 */ - 503: t.Object({ - code: t.Literal(503), - message: t.String({ - examples: ['服务暂时不可用,请稍后重试', '系统维护中', '依赖服务异常'], - }), - data: t.Null(), - }), -}; - -/** - * 全局响应包装器Schema - * @param dataSchema 数据Schema - * @returns 包装后的响应Schema - */ -export const globalResponseWrapperSchema = (dataSchema: any) => - t.Object({ - code: t.Union([ - t.Literal('SUCCESS'), - t.Literal('VALIDATION_ERROR'), - t.Literal('UNAUTHORIZED'), - t.Literal('FORBIDDEN'), - t.Literal('NOT_FOUND'), - t.Literal('METHOD_NOT_ALLOWED'), - t.Literal('CONFLICT'), - t.Literal('RATE_LIMIT_EXCEEDED'), - t.Literal('BUSINESS_ERROR'), - t.Literal('USER_NOT_FOUND'), - t.Literal('USER_ALREADY_EXISTS'), - t.Literal('INVALID_CREDENTIALS'), - t.Literal('TOKEN_EXPIRED'), - t.Literal('TOKEN_INVALID'), - t.Literal('INSUFFICIENT_PERMISSIONS'), - t.Literal('USERNAME_EXISTS'), - t.Literal('EMAIL_EXISTS'), - t.Literal('PASSWORD_MISMATCH'), - t.Literal('CAPTCHA_ERROR'), - t.Literal('EMAIL_SEND_FAILED'), - t.Literal('INVALID_ACTIVATION_TOKEN'), - t.Literal('ALREADY_ACTIVATED'), - t.Literal('INVALID_PASSWORD'), - t.Literal('ACCOUNT_NOT_ACTIVATED'), - t.Literal('ACCOUNT_LOCKED'), - t.Literal('TOO_MANY_FAILED_ATTEMPTS'), - t.Literal('INVALID_RESET_TOKEN'), - t.Literal('NOT_IMPLEMENTED'), - t.Literal('INTERNAL_ERROR'), - t.Literal('DATABASE_ERROR'), - t.Literal('REDIS_ERROR'), - t.Literal('EXTERNAL_API_ERROR'), - t.Literal('SERVICE_UNAVAILABLE'), - ], { - description: '响应状态码', - }), - message: t.String({ - description: '响应消息', - examples: ['操作成功', '获取数据成功', '创建成功'], - }), - data: dataSchema, - }); - -/** - * 健康检查响应Schema - */ -export const HealthCheckResponseSchema = t.Object({ - code: t.Number(), - message: t.String(), - data: t.Object({ - status: t.Union([ - t.Literal('healthy'), - t.Literal('unhealthy'), - t.Literal('degraded'), - ], { - description: '系统健康状态:healthy-健康,unhealthy-不健康,degraded-降级', - }), - timestamp: t.String({ - description: 'ISO时间戳', - examples: ['2024-06-28T12:00:00.000Z'], - }), - uptime: t.Number({ - description: '系统运行时间(秒)', - examples: [3600, 86400], - }), - responseTime: t.Number({ - description: '响应时间(毫秒)', - examples: [15, 50, 100], - }), - version: t.String({ - description: '系统版本', - examples: ['1.0.0', '1.2.3'], - }), - environment: t.String({ - description: '运行环境', - examples: ['development', 'production', 'test'], - }), - components: t.Object({ - mysql: t.Optional(t.Object({ - status: t.String(), - responseTime: t.Optional(t.Number()), - error: t.Optional(t.String()), - details: t.Optional(t.Any()), - })), - redis: t.Optional(t.Object({ - status: t.String(), - responseTime: t.Optional(t.Number()), - error: t.Optional(t.String()), - details: t.Optional(t.Any()), - })), - }), - }), -}); \ No newline at end of file