feat: 优化参数相应

This commit is contained in:
expressgy 2025-07-06 06:02:05 +08:00
parent 541dd50ea3
commit 1575154bfb
26 changed files with 600 additions and 2067 deletions

View File

@ -142,12 +142,6 @@ src/utils/
└── response.helper.ts # 响应格式工具 (新增)
```
- 验证器 (validators/)
```
src/validators/
└── global.response.ts # 全局响应格式验证
```
- 测试文件 (tests/)
```
src/tests/

View File

@ -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()

View File

@ -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,
}
);

View File

@ -8,13 +8,16 @@
*/
import { t, type Static } from 'elysia';
import { globalResponseWrapperSchema } from '@/validators/global.response';
import { responseWrapperSchema } from '@/utils/responseFormate';
// ========== 邮箱注册相关响应格式 ==========
/**
* Schema
* @description
*
* @description Controller中定义所有可能的响应格式
*/
export const RegisterSuccessDataSchema = t.Object({
export const RegisterResponsesSchema = {
200: responseWrapperSchema(t.Object({
/** 用户ID */
id: t.String({
description: '用户IDbigint类型以字符串形式返回防止精度丢失',
@ -40,76 +43,19 @@ export const RegisterSuccessDataSchema = t.Object({
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: ['服务器内部错误']
}),
data: t.Null()
})
})),
};
// ========== TypeScript类型导出 ==========
/** 用户注册成功响应数据类型 */
export type RegisterSuccessData = Static<typeof RegisterSuccessDataSchema>;
/** 用户注册成功响应类型 */
export type RegisterSuccessResponse = Static<typeof RegisterSuccessResponseSchema>;
/** 用户注册失败响应类型 */
export type RegisterErrorResponse = Static<typeof RegisterErrorResponseSchema>;
export type RegisterResponsesType = Static<typeof RegisterResponsesSchema[200]>;
// ========== 邮箱激活相关响应格式 ==========
/**
* Schema
* @description
*
* @description Controller中定义所有可能的响应格式
*/
export const ActivateSuccessDataSchema = t.Object({
export const ActivateResponsesSchema = {
200: responseWrapperSchema(t.Object({
/** 用户ID */
id: t.String({
description: '用户IDbigint类型以字符串形式返回防止精度丢失',
@ -140,100 +86,20 @@ export const ActivateSuccessDataSchema = t.Object({
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<typeof ActivateSuccessDataSchema>;
/** 邮箱激活成功响应类型 */
export type ActivateSuccessResponse = Static<typeof ActivateSuccessResponseSchema>;
/** 邮箱激活失败响应类型 */
export type ActivateErrorResponse = Static<typeof ActivateErrorResponseSchema>;
export type ActivateSuccessType = Static<typeof ActivateResponsesSchema[200]>;
// ========== 用户登录相关响应格式 ==========
/**
* Schema
* @description
*
* @description Controller中定义所有可能的响应格式
*/
export const LoginSuccessDataSchema = t.Object({
export const LoginResponsesSchema = {
200: responseWrapperSchema(t.Object({
/** 用户基本信息 */
user: t.Object({
/** 用户ID */
@ -290,122 +156,8 @@ export const LoginSuccessDataSchema = t.Object({
examples: [2592000]
})
})
});
/**
* Schema
* @description
*/
export const LoginSuccessResponseSchema = globalResponseWrapperSchema(LoginSuccessDataSchema);
/**
* 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: ['用户名或密码错误', '账号未激活']
}),
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']
})
})
}),
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<typeof LoginSuccessDataSchema>;
/** 用户登录成功响应类型 */
export type LoginSuccessResponse = Static<typeof LoginSuccessResponseSchema>;
/** 用户登录失败响应类型 */
export type LoginErrorResponse = Static<typeof LoginErrorResponseSchema>;
export type LoginSuccessType = Static<typeof LoginResponsesSchema[200]>;

View File

@ -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({

View File

@ -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,8 +33,7 @@ export class AuthService {
* @param request
* @returns Promise<RegisterSuccessResponse>
*/
async register(request: RegisterRequest): Promise<RegisterSuccessResponse> {
try {
public async register(request: RegisterRequest): Promise<RegisterResponsesType> {
Logger.info(`用户注册请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`);
const { username, email, password, captcha, captchaId } = request;
@ -78,16 +69,6 @@ export class AuthService {
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);
}
}
/**
@ -96,7 +77,6 @@ export class AuthService {
* @param captchaId ID
*/
private async validateCaptcha(captcha: string, captchaId: string): Promise<void> {
try {
const result = await captchaService.verifyCaptcha({
captchaId,
captchaCode: captcha
@ -105,15 +85,9 @@ export class AuthService {
if (!result.data?.valid) {
throw new BusinessError(
result.data?.message || '验证码验证失败',
ERROR_CODES.CAPTCHA_ERROR
400
);
}
} catch (error) {
if (error instanceof BusinessError) {
throw error;
}
throw new BusinessError('验证码验证失败', ERROR_CODES.CAPTCHA_ERROR);
}
}
/**
@ -121,21 +95,13 @@ export class AuthService {
* @param username
*/
private async checkUsernameExists(username: string): Promise<void> {
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);
throw new BusinessError('用户名已存在', 400);
}
}
@ -144,21 +110,13 @@ export class AuthService {
* @param email
*/
private async checkEmailExists(email: string): Promise<void> {
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);
throw new BusinessError('邮箱已被注册', 400);
}
}
@ -168,12 +126,7 @@ export class AuthService {
* @returns Promise<string>
*/
private async hashPassword(password: string): Promise<string> {
try {
return await bcrypt.hash(password, this.BCRYPT_ROUNDS);
} catch (error) {
Logger.error(new Error(`密码加密失败:${error}`));
throw new BusinessError('密码加密失败', ERROR_CODES.INTERNAL_ERROR);
}
}
/**
@ -192,13 +145,12 @@ 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({
await db().insert(sysUsers).values({
id: userId,
username,
email,
@ -219,23 +171,18 @@ export class AuthService {
.limit(1);
Logger.info(`创建用户成功: ${JSON.stringify(newUser, null, 2)}`);
if (!newUser) {
throw new Error('创建用户后查询失败');
}
// 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
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);
}
}
/**
@ -243,28 +190,33 @@ export class AuthService {
* @param request
* @returns Promise<ActivateSuccessResponse>
*/
async activate(request: ActivateRequest): Promise<ActivateSuccessResponse> {
try {
public async activate(request: ActivateRequest): Promise<ActivateSuccessType> {
Logger.info(`邮箱激活请求开始处理`);
const { token } = request;
// 1. 验证激活Token
const tokenPayload = await this.validateActivationToken(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('账号已经激活', ERROR_CODES.ALREADY_ACTIVATED);
throw new BusinessError('账号已经激活', 400);
}
// 4. 更新用户状态为激活
const activatedUser = await this.updateUserStatus(tokenPayload.userId, 'active');
// 5. 发送激活成功邮件(可选)
await this.sendActivationSuccessEmail(activatedUser.email, activatedUser.username);
this.sendActivationSuccessEmail(activatedUser.email, activatedUser.username);
Logger.info(`用户邮箱激活成功:${activatedUser.id} - ${activatedUser.email}`);
@ -276,62 +228,6 @@ export class AuthService {
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);
}
}
/**
* Token
* @param token Token
* @returns Promise<ActivationTokenPayload> Token载荷
*/
private async validateActivationToken(token: string): Promise<any> {
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);
}
}
/**
@ -347,7 +243,6 @@ export class AuthService {
createdAt: string;
updatedAt: string;
}> {
try {
const [user] = await db().select({
id: sysUsers.id,
username: sysUsers.username,
@ -361,7 +256,7 @@ export class AuthService {
.limit(1);
if (!user) {
throw new BusinessError('用户不存在', ERROR_CODES.USER_NOT_FOUND);
throw new BusinessError('用户不存在', 400);
}
return {
@ -372,14 +267,6 @@ export class AuthService {
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);
}
}
/**
@ -395,11 +282,11 @@ export class AuthService {
status: string;
updatedAt: string;
}> {
try {
// 更新用户状态
await db().update(sysUsers)
.set({
status: status,})
status: status,
})
.where(eq(sysUsers.id, BigInt(userId)));
// 查询更新后的用户信息
@ -414,23 +301,13 @@ export class AuthService {
.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
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);
}
}
/**
@ -438,15 +315,14 @@ export class AuthService {
* @param request
* @returns Promise<LoginSuccessResponse>
*/
async login(request: LoginRequest): Promise<LoginSuccessResponse> {
try {
async login(request: LoginRequest): Promise<LoginSuccessType> {
Logger.info(`用户登录请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`);
const { identifier, password, captcha, captchaId, rememberMe = false } = request;
// 1. 验证验证码(如果提供)
if (captcha && captchaId) {
// await this.validateCaptcha(captcha, captchaId);
await this.validateCaptcha(captcha, captchaId);
}
// 2. 查找用户(支持用户名或邮箱)
@ -471,8 +347,6 @@ export class AuthService {
Logger.info(`用户登录成功:${user.id} - ${user.username}`);
console.log(tokens);
return successResponse({
user: {
id: user.id,
@ -483,16 +357,6 @@ export class AuthService {
},
tokens
}, '登录成功');
} catch (error) {
Logger.error(new Error(`用户登录失败:${error}`));
if (error instanceof BusinessError) {
throw error;
}
throw new BusinessError('登录失败,请稍后重试', ERROR_CODES.INTERNAL_ERROR);
}
}
/**
@ -508,7 +372,6 @@ export class AuthService {
passwordHash: string;
lastLoginAt: string | null;
}> {
try {
// 判断是否为邮箱格式
const isEmail = identifier.includes('@');
@ -530,25 +393,17 @@ export class AuthService {
.limit(1);
if (!user) {
throw new BusinessError('用户不存在', ERROR_CODES.USER_NOT_FOUND);
throw new BusinessError('用户不存在', 400);
}
return {
id: user.id.toString(), // 转换为字符串避免精度丢失
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);
}
}
/**
@ -557,20 +412,11 @@ export class AuthService {
* @param passwordHash
*/
private async verifyPassword(password: string, passwordHash: string): Promise<void> {
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);
throw new BusinessError('密码错误', 400);
}
}
@ -578,17 +424,17 @@ export class AuthService {
*
* @param user
*/
private async checkAccountStatus(user: { status: string }): Promise<void> {
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<void> {
try {
await db().update(sysUsers)
.set({ lastLoginAt: new Date().toISOString() })
.set({
lastLoginAt: sql`NOW()`, // 使用 MySQL 的 NOW() 函数
loginCount: sql`${sysUsers.loginCount} + 1`
})
.where(eq(sysUsers.id, BigInt(userId)));
} catch (error) {
// 记录错误但不影响登录流程
Logger.error(new Error(`更新最后登录时间失败:${error}`));
}
}
/**
@ -649,8 +492,7 @@ export class AuthService {
<p>使</p>
`
});
Logger.info(`激活成功邮件发送成功:${email}`);
// Logger.info(`激活成功邮件发送成功:${email}`);
} catch (error) {
// 邮件发送失败不影响激活流程,只记录日志
@ -669,7 +511,7 @@ export class AuthService {
// 生成激活Token载荷
const activationTokenPayload = await jwtService.generateActivationToken(userId, email, username);
Logger.debug({activationTokenPayload});
Logger.debug({ activationTokenPayload });
// 发送激活邮件
await emailService.sendEmail({
to: email,
@ -684,7 +526,6 @@ export class AuthService {
});
Logger.info(`激活邮件发送成功:${email}`);
} catch (error) {
// 邮件发送失败不影响注册流程,只记录日志
Logger.warn(`激活邮件发送失败:${email}, 错误:${error}`);

View File

@ -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())},
}
);

View File

@ -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<typeof GenerateCaptchaSuccessResponseSchema>;
/**
*
*/
export const VerifyCaptchaSuccessResponseSchema = globalResponseWrapperSchema(t.Object({
valid: t.Boolean({ description: '验证结果' }),
message: t.String({ description: '验证消息' })
}));
export type VerifyCaptchaSuccessResponse = Static<typeof VerifyCaptchaSuccessResponseSchema>;
/**
*
*/
export const CaptchaNotFoundResponseSchema = t.Object({
code: t.Literal('CAPTCHA_NOT_FOUND'),
message: t.String({ examples: ['验证码不存在或已过期'] }),
data: t.Null(),
});
export type CaptchaNotFoundResponse = Static<typeof CaptchaNotFoundResponseSchema>;
/**
*
*/
export const CaptchaInvalidResponseSchema = t.Object({
code: t.Literal('CAPTCHA_INVALID'),
message: t.String({ examples: ['验证码错误'] }),
data: t.Null(),
});
export type CaptchaInvalidResponse = Static<typeof CaptchaInvalidResponseSchema>;
/**
*
*/
export const CaptchaExpiredResponseSchema = t.Object({
code: t.Literal('CAPTCHA_EXPIRED'),
message: t.String({ examples: ['验证码已过期'] }),
data: t.Null(),
});
export type CaptchaExpiredResponse = Static<typeof CaptchaExpiredResponseSchema>;
/**
*
*/
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(),
}),
};

View File

@ -13,14 +13,9 @@ import type {
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,17 +23,14 @@ export class CaptchaService {
* @param request
* @returns Promise<GenerateCaptchaSuccessResponse>
*/
async generateCaptcha(request: GenerateCaptchaRequest): Promise<GenerateCaptchaSuccessResponse> {
try {
Logger.info(`生成验证码请求:${JSON.stringify(request)}`);
async generateCaptcha(body: GenerateCaptchaRequest) {
const {
type = 'image',
width = 200,
height = 60,
length = 4,
expireTime = 300
} = request;
} = body;
// 生成验证码ID
const captchaId = `captcha_${randomBytes(16).toString('hex')}`;
@ -70,7 +62,7 @@ export class CaptchaService {
const redisKey = `captcha:${captchaId}`;
await redisService.setex(redisKey, expireTime, JSON.stringify(captchaData));
Logger.info(`验证码生成成功:${captchaId}`);
Logger.info(`验证码生成成功:${captchaId} ${code}`);
// 构建响应数据
const responseData: CaptchaGenerateResponse = {
@ -80,11 +72,7 @@ export class CaptchaService {
type
};
return successResponse(responseData, '验证码生成成功');
} catch (error) {
Logger.error(new Error(`生成验证码失败:${error}`));
throw error;
}
return successResponse(responseData);
}
/**
@ -92,10 +80,7 @@ export class CaptchaService {
* @param request
* @returns Promise<VerifyCaptchaSuccessResponse>
*/
async verifyCaptcha(request: VerifyCaptchaRequest): Promise<VerifyCaptchaSuccessResponse> {
try {
Logger.info(`验证验证码请求:${JSON.stringify(request)}`);
async verifyCaptcha(request: VerifyCaptchaRequest) {
const { captchaId, captchaCode, scene } = request;
// 从Redis获取验证码数据
@ -103,24 +88,15 @@ export class CaptchaService {
const captchaDataStr = await redisService.get(redisKey);
if (!captchaDataStr) {
Logger.warn(`验证码不存在:${captchaId}`);
return successResponse(
{ valid: false, message: '验证码不存在或已过期' },
'验证失败'
);
throw new BusinessError('验证码不存在或已过期', 400);
}
const captchaData: CaptchaData = JSON.parse(captchaDataStr);
// 检查是否过期
if (Date.now() > captchaData.expireTime) {
Logger.warn(`验证码已过期:${captchaId}`);
// 删除过期的验证码
await redisService.del(redisKey);
return successResponse(
{ valid: false, message: '验证码已过期' },
'验证失败'
);
throw new BusinessError('验证码已过期:', 400);
}
// 验证验证码内容(忽略大小写)
@ -131,19 +107,10 @@ export class CaptchaService {
await redisService.del(redisKey);
Logger.info(`验证码验证成功:${captchaId}`);
return successResponse(
{ valid: true, message: '验证码验证成功' },
'验证成功'
{ valid: true }, '验证码验证成功'
);
} else {
Logger.warn(`验证码错误:${captchaId},输入:${captchaCode},正确:${captchaData.code}`);
return successResponse(
{ valid: false, message: '验证码错误' },
'验证失败'
);
}
} catch (error) {
Logger.error(new Error(`验证验证码失败:${error}`));
throw error;
throw new BusinessError('验证码错误', 400);
}
}
@ -230,8 +197,7 @@ export class CaptchaService {
*
* @returns Promise<number>
*/
async cleanupExpiredCaptchas(): Promise<number> {
try {
async cleanupExpiredCaptchas() {
const pattern = 'captcha:*';
const keys = await redisService.keys(pattern);
let cleanedCount = 0;
@ -248,11 +214,9 @@ export class CaptchaService {
}
Logger.info(`清理过期验证码完成,共清理 ${cleanedCount}`);
return cleanedCount;
} catch (error) {
Logger.error(new Error(`清理过期验证码失败:${error}`));
throw error;
}
return successResponse(
{ cleanedCount }, '清理完成'
);
}
}

View File

@ -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,
},
);

View File

@ -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],
};

View File

@ -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<typeof GetUserByUsernameSchema>;
/**
*
*/
export type UserInfoType = Static<typeof UserInfoSchema>;

View File

@ -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();

View File

@ -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');
});
});
});

View File

@ -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))
// 验证码接口

View File

@ -20,8 +20,6 @@ export const tags = {
health: 'Health',
/** 测试接口 */
test: 'Test',
/** 样例接口 */
example: 'example',
/** 文件上传接口 */
upload: 'Upload',
/** 系统管理接口 */

View File

@ -192,7 +192,6 @@ export class DrizzleService {
message: 'SQL查询执行',
query: query.replace(/\s+/g, ' ').trim(),
params: params,
timestamp: new Date().toISOString(),
});
},
} : false,

View File

@ -123,7 +123,7 @@ export class EmailService {
public async initialize(): Promise<EmailTransporter> {
// 防止重复初始化
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('关闭邮件服务时出错'));
}

View File

@ -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: '服务器内部错误',

View File

@ -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;
}
}
}

View File

@ -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)}`;
}),
),
});

View File

@ -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: '基础响应结构',

View File

@ -60,6 +60,7 @@ export interface JwtPayloadType extends JwtUserType {
jti?: string;
/** Token生效时间秒级时间戳 */
nbf?: number;
error?: boolean;
}
/**

View File

@ -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<T = any> {
/** 业务状态码 */
code: ErrorCode;
/** 响应消息 */
message: string;
/** 响应数据 */
data: T;
/** 时间戳 */
timestamp?: string;
/** 请求ID可选用于追踪 */
requestId?: string;
}
/**
*
* @param data
* @param message 使
* @returns
*/
export function successResponse<T>(
data: T,
message: string = ERROR_CODE_MESSAGES[ERROR_CODES.SUCCESS]
): ApiResponse<T> {
return {
code: ERROR_CODES.SUCCESS,
message,
data,
timestamp: new Date().toISOString(),
};
}
/**
*
* @param code
* @param message 使
* @param data null
* @returns
*/
export function errorResponse<T = null>(
code: ErrorCode,
message?: string,
data: T = null as T
): ApiResponse<T> {
return {
code,
message: message || ERROR_CODE_MESSAGES[code] || '未知错误',
data,
timestamp: new Date().toISOString(),
};
}
/**
*
*/
export interface PaginatedData<T> {
/** 数据列表 */
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<T>(
items: T[],
total: number,
page: number,
pageSize: number,
message: string = '查询成功'
): ApiResponse<PaginatedData<T>> {
const totalPages = Math.ceil(total / pageSize);
return successResponse<PaginatedData<T>>({
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';
}
}

View File

@ -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,
});

View File

@ -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()),
})),
}),
}),
});