feat: 优化参数相应
This commit is contained in:
parent
541dd50ea3
commit
1575154bfb
@ -142,12 +142,6 @@ src/utils/
|
||||
└── response.helper.ts # 响应格式工具 (新增)
|
||||
```
|
||||
|
||||
- 验证器 (validators/)
|
||||
```
|
||||
src/validators/
|
||||
└── global.response.ts # 全局响应格式验证
|
||||
```
|
||||
|
||||
- 测试文件 (tests/)
|
||||
```
|
||||
src/tests/
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
);
|
@ -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<typeof RegisterSuccessDataSchema>;
|
||||
|
||||
/** 用户注册成功响应类型 */
|
||||
export type RegisterSuccessResponse = Static<typeof RegisterSuccessResponseSchema>;
|
||||
|
||||
/** 用户注册失败响应类型 */
|
||||
export type RegisterErrorResponse = Static<typeof RegisterErrorResponseSchema>;
|
||||
export type RegisterResponsesType = Static<typeof RegisterResponsesSchema[200]>;
|
||||
|
||||
// ========== 邮箱激活相关响应格式 ==========
|
||||
|
||||
/**
|
||||
* 邮箱激活成功响应数据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<typeof ActivateSuccessDataSchema>;
|
||||
|
||||
/** 邮箱激活成功响应类型 */
|
||||
export type ActivateSuccessResponse = Static<typeof ActivateSuccessResponseSchema>;
|
||||
|
||||
/** 邮箱激活失败响应类型 */
|
||||
export type ActivateErrorResponse = Static<typeof ActivateErrorResponseSchema>;
|
||||
|
||||
// ========== 用户登录相关响应格式 ==========
|
||||
|
||||
/**
|
||||
* 用户登录成功响应数据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<typeof ActivateResponsesSchema[200]>;
|
||||
|
||||
/**
|
||||
* 用户登录失败响应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<typeof LoginSuccessDataSchema>;
|
||||
|
||||
/** 用户登录成功响应类型 */
|
||||
export type LoginSuccessResponse = Static<typeof LoginSuccessResponseSchema>;
|
||||
|
||||
/** 用户登录失败响应类型 */
|
||||
export type LoginErrorResponse = Static<typeof LoginErrorResponseSchema>;
|
||||
export type LoginSuccessType = Static<typeof LoginResponsesSchema[200]>;
|
@ -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({
|
||||
|
@ -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<RegisterSuccessResponse>
|
||||
*/
|
||||
async register(request: RegisterRequest): Promise<RegisterSuccessResponse> {
|
||||
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<RegisterResponsesType> {
|
||||
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<void> {
|
||||
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<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);
|
||||
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<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);
|
||||
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<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);
|
||||
}
|
||||
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<ActivateSuccessResponse>
|
||||
*/
|
||||
async activate(request: ActivateRequest): Promise<ActivateSuccessResponse> {
|
||||
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<ActivateSuccessType> {
|
||||
Logger.info(`邮箱激活请求开始处理`);
|
||||
|
||||
/**
|
||||
* 验证激活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);
|
||||
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<LoginSuccessResponse>
|
||||
*/
|
||||
async login(request: LoginRequest): Promise<LoginSuccessResponse> {
|
||||
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<LoginSuccessType> {
|
||||
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<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);
|
||||
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<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() })
|
||||
.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<void> {
|
||||
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 {
|
||||
<p>感谢您的使用!</p>
|
||||
`
|
||||
});
|
||||
|
||||
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 {
|
||||
<p>如果您没有注册,请忽略此邮件。</p>
|
||||
`
|
||||
});
|
||||
|
||||
|
||||
Logger.info(`激活邮件发送成功:${email}`);
|
||||
|
||||
} catch (error) {
|
||||
// 邮件发送失败不影响注册流程,只记录日志
|
||||
Logger.warn(`激活邮件发送失败:${email}, 错误:${error}`);
|
||||
|
@ -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())},
|
||||
}
|
||||
);
|
@ -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(),
|
||||
}),
|
||||
};
|
@ -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<GenerateCaptchaSuccessResponse>
|
||||
*/
|
||||
async generateCaptcha(request: GenerateCaptchaRequest): Promise<GenerateCaptchaSuccessResponse> {
|
||||
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<VerifyCaptchaSuccessResponse>
|
||||
*/
|
||||
async verifyCaptcha(request: VerifyCaptchaRequest): Promise<VerifyCaptchaSuccessResponse> {
|
||||
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<number> 清理的验证码数量
|
||||
*/
|
||||
async cleanupExpiredCaptchas(): Promise<number> {
|
||||
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 }, '清理完成'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
@ -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],
|
||||
};
|
@ -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>;
|
@ -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();
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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))
|
||||
// 验证码接口
|
||||
|
@ -20,8 +20,6 @@ export const tags = {
|
||||
health: 'Health',
|
||||
/** 测试接口 */
|
||||
test: 'Test',
|
||||
/** 样例接口 */
|
||||
example: 'example',
|
||||
/** 文件上传接口 */
|
||||
upload: 'Upload',
|
||||
/** 系统管理接口 */
|
||||
|
@ -192,7 +192,6 @@ export class DrizzleService {
|
||||
message: 'SQL查询执行',
|
||||
query: query.replace(/\s+/g, ' ').trim(),
|
||||
params: params,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
} : false,
|
||||
|
@ -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('关闭邮件服务时出错'));
|
||||
}
|
||||
|
@ -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: '服务器内部错误',
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)}`;
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
@ -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: '基础响应结构',
|
||||
|
@ -60,6 +60,7 @@ export interface JwtPayloadType extends JwtUserType {
|
||||
jti?: string;
|
||||
/** Token生效时间(秒级时间戳) */
|
||||
nbf?: number;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
67
src/utils/responseFormate.ts
Normal file
67
src/utils/responseFormate.ts
Normal 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,
|
||||
});
|
@ -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()),
|
||||
})),
|
||||
}),
|
||||
}),
|
||||
});
|
Loading…
Reference in New Issue
Block a user