refactor:项目架构重构和命名规范化

- 统一文件命名为snake_case格式(kebab-case  snake_case)
- 重构zulip模块为zulip_core,明确Core层职责
- 重构user-mgmt模块为user_mgmt,统一命名规范
- 调整模块依赖关系,优化架构分层
- 删除过时的文件和目录结构
- 更新相关文档和配置文件

本次重构涉及大量文件重命名和模块重组,
旨在建立更清晰的项目架构和统一的命名规范。
This commit is contained in:
moyin
2026-01-08 00:14:14 +08:00
parent 4fa4bd1a70
commit bb796a2469
178 changed files with 24767 additions and 3484 deletions

View File

@@ -0,0 +1,92 @@
# Email 邮件服务模块
Email 是应用的核心邮件发送模块,提供完整的邮件发送技术能力,支持多种邮件模板和场景,为业务层提供可靠的邮件通信服务。
## 邮件发送功能
### sendEmail()
通用邮件发送方法支持HTML和纯文本格式提供灵活的邮件发送能力。
### sendVerificationCode()
发送验证码邮件,支持邮箱验证、密码重置、登录验证三种用途,自动选择对应模板。
### sendWelcomeEmail()
发送欢迎邮件,包含游戏特色介绍,用于新用户注册成功后的欢迎通知。
## 服务管理功能
### verifyConnection()
验证邮件服务连接状态检查SMTP服务器连接和认证信息是否有效。
### isTestMode()
检查当前是否为测试模式,用于区分开发环境和生产环境的邮件发送行为。
## 使用的项目内部依赖
### Injectable (来自 @nestjs/common)
NestJS依赖注入装饰器将EmailService注册为可注入的服务。
### Logger (来自 @nestjs/common)
NestJS日志服务用于记录邮件发送过程和错误信息。
### ConfigService (来自 @nestjs/config)
NestJS配置服务用于读取邮件服务相关的环境变量配置。
### nodemailer (来自 第三方库)
Node.js邮件发送库提供SMTP传输器和邮件发送核心功能。
### EmailOptions (本模块)
邮件发送选项接口,定义邮件的基本参数(收件人、主题、内容等)。
### VerificationEmailOptions (本模块)
验证码邮件选项接口,定义验证码邮件的特定参数(邮箱、验证码、用途等)。
### EmailSendResult (本模块)
邮件发送结果接口,定义发送结果的状态信息(成功状态、测试模式、错误信息)。
## 核心特性
### 双模式支持
- 生产模式使用真实SMTP服务器发送邮件到用户邮箱
- 测试模式:输出邮件内容到控制台,不真实发送邮件
- 自动检测:根据环境变量配置自动切换运行模式
### 多模板支持
- 邮箱验证模板:蓝色主题,用于用户注册时的邮箱验证
- 密码重置模板:红色主题,用于密码找回功能
- 登录验证模板:蓝色主题,用于验证码登录
- 欢迎邮件模板:绿色主题,用于新用户注册成功通知
### 配置灵活性
- 支持多种SMTP服务商Gmail、163邮箱、QQ邮箱等
- 可配置主机、端口、安全设置、认证信息
- 提供合理的默认配置值,简化部署过程
- 支持自定义发件人信息和邮件签名
### 错误处理机制
- 完善的异常捕获和错误日志记录
- 网络错误、认证错误的分类处理
- 发送失败时返回详细的错误信息
- 连接验证功能确保服务可用性
## 潜在风险
### 配置依赖风险
- 邮件服务依赖外部SMTP服务器配置配置错误会导致发送失败
- 未配置邮件服务时自动降级为测试模式,生产环境需要确保正确配置
- 建议在应用启动时验证邮件服务配置,在部署前进行连接测试
### 网络连接风险
- SMTP服务器连接可能因网络问题、防火墙设置等原因失败
- 第三方邮件服务可能有发送频率限制或IP被封禁的风险
- 建议配置多个备用SMTP服务商实现故障转移机制
### 模板维护风险
- HTML邮件模板较长且包含内联样式维护时容易出错
- 模板中的品牌信息、联系方式等内容需要定期更新
- 建议将邮件模板提取到独立的模板文件中,便于统一管理和维护
### 安全风险
- SMTP认证信息存储在环境变量中需要确保配置文件安全
- 邮件内容可能包含敏感信息(验证码等),需要注意传输安全
- 建议使用加密连接TLS/SSL和强密码策略

View File

@@ -6,9 +6,17 @@
* - 导出邮件服务供其他模块使用
* - 集成配置服务
*
* 职责分离:
* - 模块配置:定义邮件服务的依赖和导出
* - 服务集成整合ConfigModule和EmailService
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Module } from '@nestjs/common';

View File

@@ -9,9 +9,18 @@
* - 邮件模板生成
* - 连接验证
*
* 职责分离:
* - 单元测试:测试各个方法的功能正确性
* - Mock测试模拟外部依赖进行隔离测试
* - 异常测试:验证错误处理机制
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Test, TestingModule } from '@nestjs/testing';

View File

@@ -12,12 +12,22 @@
* - 欢迎邮件
* - 系统通知
*
* 职责分离:
* - 邮件发送:核心邮件发送功能实现
* - 模板管理:各种邮件模板的生成和管理
* - 配置管理:邮件服务配置和连接管理
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 清理未使用的导入(BadRequestException),移除多余注释
* - 2026-01-07: 代码规范优化 - 完善方法注释和修改记录
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
import { Transporter } from 'nodemailer';
@@ -51,7 +61,7 @@ export interface VerificationEmailOptions {
}
/**
* 邮件发送结果接口 by angjustinl 2025-12-17
* 邮件发送结果接口
*/
export interface EmailSendResult {
/** 是否成功 */
@@ -62,6 +72,27 @@ export interface EmailSendResult {
error?: string;
}
/**
* 邮件服务类
*
* 职责:
* - 邮件发送功能:提供统一的邮件发送接口
* - 模板管理:管理各种邮件模板(验证码、欢迎邮件等)
* - 配置管理:处理邮件服务配置和连接
* - 测试模式:支持开发环境的邮件测试模式
*
* 主要方法:
* - sendEmail() - 通用邮件发送方法
* - sendVerificationCode() - 发送验证码邮件
* - sendWelcomeEmail() - 发送欢迎邮件
* - verifyConnection() - 验证邮件服务连接
*
* 使用场景:
* - 用户注册时发送邮箱验证码
* - 密码重置时发送重置验证码
* - 用户注册成功后发送欢迎邮件
* - 登录验证时发送登录验证码
*/
@Injectable()
export class EmailService {
private readonly logger = new Logger(EmailService.name);
@@ -73,6 +104,14 @@ export class EmailService {
/**
* 初始化邮件传输器
*
* 业务逻辑:
* 1. 从配置服务获取邮件服务配置(主机、端口、安全设置、认证信息)
* 2. 检查是否配置了用户名和密码
* 3. 未配置创建测试模式传输器streamTransport
* 4. 已配置创建真实SMTP传输器
* 5. 记录初始化结果到日志
* 6. 设置transporter实例
*/
private initializeTransporter(): void {
const emailConfig = {
@@ -102,7 +141,20 @@ export class EmailService {
/**
* 检查是否为测试模式
*
* @returns 是否为测试模式
* 业务逻辑:
* 1. 检查transporter的options配置
* 2. 判断是否设置了streamTransport选项
* 3. streamTransport为true表示测试模式
* 4. 返回测试模式状态
*
* @returns 是否为测试模式true表示测试模式false表示生产模式
*
* @example
* ```typescript
* if (emailService.isTestMode()) {
* console.log('当前为测试模式,邮件不会真实发送');
* }
* ```
*/
isTestMode(): boolean {
return !!(this.transporter.options as any).streamTransport;
@@ -111,8 +163,30 @@ export class EmailService {
/**
* 发送邮件
*
* 业务逻辑:
* 1. 构建邮件选项(发件人、收件人、主题、内容)
* 2. 检查是否为测试模式
* 3. 测试模式:输出邮件内容到控制台,不真实发送
* 4. 生产模式通过SMTP服务器发送邮件
* 5. 记录发送结果和错误信息
* 6. 返回发送结果状态
*
* @param options 邮件选项
* @returns 发送结果
* @returns 发送结果,包含成功状态、测试模式标识和错误信息
* @throws Error 当邮件发送失败时抛出错误(已捕获并返回在结果中)
*
* @example
* ```typescript
* const result = await emailService.sendEmail({
* to: 'user@example.com',
* subject: '测试邮件',
* html: '<p>邮件内容</p>',
* text: '邮件内容'
* });
* if (result.success) {
* console.log('邮件发送成功');
* }
* ```
*/
async sendEmail(options: EmailOptions): Promise<EmailSendResult> {
try {
@@ -155,8 +229,28 @@ export class EmailService {
/**
* 发送邮箱验证码
*
* 业务逻辑:
* 1. 根据验证码用途选择对应的邮件主题和模板
* 2. 邮箱验证:使用邮箱验证模板
* 3. 密码重置:使用密码重置模板
* 4. 登录验证:使用登录验证模板
* 5. 生成HTML邮件内容和纯文本内容
* 6. 调用sendEmail方法发送邮件
* 7. 返回发送结果
*
* @param options 验证码邮件选项
* @returns 发送结果
* @returns 发送结果,包含成功状态和错误信息
* @throws Error 当邮件发送失败时(已捕获并返回在结果中)
*
* @example
* ```typescript
* const result = await emailService.sendVerificationCode({
* email: 'user@example.com',
* code: '123456',
* nickname: '张三',
* purpose: 'email_verification'
* });
* ```
*/
async sendVerificationCode(options: VerificationEmailOptions): Promise<EmailSendResult> {
const { email, code, nickname, purpose } = options;
@@ -189,9 +283,25 @@ export class EmailService {
/**
* 发送欢迎邮件
*
* 业务逻辑:
* 1. 设置欢迎邮件主题
* 2. 生成包含用户昵称的欢迎邮件模板
* 3. 模板包含游戏特色介绍(建造创造、社交互动、任务挑战)
* 4. 调用sendEmail方法发送邮件
* 5. 返回发送结果
*
* @param email 邮箱地址
* @param nickname 用户昵称
* @returns 发送结果
* @returns 发送结果,包含成功状态和错误信息
* @throws Error 当邮件发送失败时(已捕获并返回在结果中)
*
* @example
* ```typescript
* const result = await emailService.sendWelcomeEmail(
* 'newuser@example.com',
* '新用户'
* );
* ```
*/
async sendWelcomeEmail(email: string, nickname: string): Promise<EmailSendResult> {
const subject = '🎮 欢迎加入 Whale Town';
@@ -453,7 +563,25 @@ export class EmailService {
/**
* 验证邮件服务配置
*
* @returns 验证结果
* 业务逻辑:
* 1. 调用transporter的verify方法测试连接
* 2. 验证SMTP服务器连接是否正常
* 3. 验证认证信息是否有效
* 4. 记录验证结果到日志
* 5. 返回验证结果状态
*
* @returns 验证结果true表示连接成功false表示连接失败
* @throws Error 当连接验证失败时已捕获并返回false
*
* @example
* ```typescript
* const isConnected = await emailService.verifyConnection();
* if (isConnected) {
* console.log('邮件服务连接正常');
* } else {
* console.log('邮件服务连接失败');
* }
* ```
*/
async verifyConnection(): Promise<boolean> {
try {

View File

@@ -0,0 +1,101 @@
# Logger 日志系统模块
Logger 是应用的核心日志管理模块,提供统一的日志记录服务、高性能日志输出、敏感信息过滤和智能日志管理功能,支持多种环境配置和请求链路追踪。
## 日志记录接口
### debug()
记录调试信息,主要用于开发环境的问题排查。
### info()
记录重要业务操作和系统状态变更,用于业务监控和审计。
### warn()
记录需要关注但不影响正常业务流程的警告信息。
### error()
记录影响业务功能正常使用的错误信息,包含详细的错误上下文和堆栈信息。
### fatal()
记录可能导致系统不可用的严重错误,需要立即处理。
### trace()
记录极细粒度的执行追踪信息,用于深度调试和性能分析。
## 上下文绑定接口
### bindRequest()
创建绑定了特定请求上下文的日志记录器,自动携带请求相关信息。
## 日志管理接口
### cleanupOldLogs()
定期清理过期日志文件每天凌晨2点自动执行。
### getLogStatistics()
获取日志统计信息,包括文件数量、大小等信息。
### getRuntimeLogTail()
获取运行日志尾部内容,用于后台查看最新日志。
## 使用的项目内部依赖
### PinoLogger (来自 nestjs-pino)
高性能日志库,提供结构化日志输出和多种传输方式。
### ConfigService (来自 @nestjs/config)
环境配置服务,用于读取日志相关的环境变量配置。
### ScheduleModule (来自 @nestjs/schedule)
定时任务模块,用于执行日志清理和健康监控任务。
### LogLevel (本模块)
日志级别类型定义包含debug、info、warn、error、fatal、trace。
### LogContext (本模块)
日志上下文接口,用于补充日志的上下文信息。
### LogOptions (本模块)
日志选项接口,定义日志记录时的参数结构。
### LoggerConfigFactory (本模块)
日志配置工厂类根据环境变量生成Pino日志配置。
## 核心特性
### 高性能日志系统
- 集成Pino高性能日志库支持降级到NestJS内置Logger
- 支持多种传输方式:控制台美化输出、文件输出、多目标输出
- 根据环境自动调整日志级别和输出策略
### 安全与隐私保护
- 自动过滤敏感信息防止密码、token等敏感数据泄露
- 递归扫描日志数据中的敏感字段,将其替换为占位符
- 支持自定义敏感字段关键词列表
### 请求链路追踪
- 支持请求上下文绑定,便于链路追踪和问题定位
- 自动生成请求ID关联用户行为和操作记录
- 提供完整的请求响应日志序列化
### 智能日志管理
- 定期清理过期日志文件,防止磁盘空间耗尽
- 监控日志系统健康状态,及时发现异常情况
- 提供日志统计和分析功能,支持运维监控
## 潜在风险
### 性能风险
- 高频日志输出可能影响应用性能特别是trace级别日志
- 建议生产环境禁用debug和trace级别仅在开发环境使用
- 大量日志文件可能占用过多磁盘空间,需要定期清理
### 配置风险
- 日志级别配置错误可能导致重要信息丢失或性能问题
- 日志目录权限不足可能导致日志写入失败
- 建议定期检查日志配置和目录权限
### 敏感信息泄露风险
- 虽然有敏感信息过滤机制,但可能存在遗漏的敏感字段
- 建议定期审查敏感字段关键词列表,确保覆盖所有敏感信息
- 避免在日志中记录完整的用户输入数据

View File

@@ -0,0 +1,393 @@
/**
* 日志管理服务测试
*
* 功能描述:
* - 测试日志管理服务的核心功能
* - 验证定时任务的执行逻辑
* - 测试日志统计和分析功能
* - 验证日志文件操作的正确性
*
* 职责分离:
* - 功能测试:测试日志管理的各项核心功能
* - 文件操作测试:验证日志文件的读写和清理
* - 统计分析测试:测试日志统计数据的准确性
* - 边界测试:验证各种边界条件和异常情况
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 新增日志管理服务测试文件
*
* 测试覆盖:
* - 服务实例化
* - 日志目录路径获取
* - 日志清理任务
* - 日志统计功能
* - 日志尾部读取
* - 配置解析功能
*
* @author moyin
* @version 1.0.1
* @since 2026-01-07
* @lastModified 2026-01-07
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { LogManagementService } from './log_management.service';
import { AppLoggerService } from './logger.service';
import * as fs from 'fs';
import * as path from 'path';
// Mock fs module
jest.mock('fs');
jest.mock('path');
describe('LogManagementService', () => {
let service: LogManagementService;
let mockConfigService: jest.Mocked<ConfigService>;
let mockLogger: jest.Mocked<AppLoggerService>;
let mockFs: jest.Mocked<typeof fs>;
let mockPath: jest.Mocked<typeof path>;
beforeEach(async () => {
// Reset mocks
jest.clearAllMocks();
// Setup mocks
mockFs = fs as jest.Mocked<typeof fs>;
mockPath = path as jest.Mocked<typeof path>;
mockConfigService = {
get: jest.fn((key: string, defaultValue?: any) => {
const config: Record<string, any> = {
LOG_DIR: './test-logs',
LOG_MAX_FILES: '7d',
LOG_MAX_SIZE: '10m',
NODE_ENV: 'test',
};
return config[key] || defaultValue;
}),
} as any;
mockLogger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
fatal: jest.fn(),
trace: jest.fn(),
} as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
LogManagementService,
{
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: AppLoggerService,
useValue: mockLogger,
},
],
}).compile();
service = module.get<LogManagementService>(LogManagementService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
/**
* 测试获取日志目录绝对路径功能
*
* 验证点:
* - getLogDirAbsolutePath 方法能够正确返回绝对路径
* - 调用了 path.resolve 方法
* - 使用了正确的日志目录配置
*/
describe('getLogDirAbsolutePath', () => {
it('should return absolute path of log directory', () => {
// Arrange
const expectedPath = '/absolute/test-logs';
mockPath.resolve.mockReturnValue(expectedPath);
// Act
const result = service.getLogDirAbsolutePath();
// Assert
expect(result).toBe(expectedPath);
expect(mockPath.resolve).toHaveBeenCalledWith('./test-logs');
});
});
/**
* 测试日志清理任务功能
*
* 验证点:
* - cleanupOldLogs 方法能够正确执行清理逻辑
* - 检查日志目录是否存在
* - 正确处理文件扫描和删除
* - 记录清理结果日志
*/
describe('cleanupOldLogs', () => {
it('should skip cleanup when log directory does not exist', async () => {
// Arrange
mockFs.existsSync.mockReturnValue(false);
// Act
await service.cleanupOldLogs();
// Assert
expect(mockFs.existsSync).toHaveBeenCalledWith('./test-logs');
expect(mockLogger.warn).toHaveBeenCalledWith(
'日志目录不存在,跳过清理任务',
expect.objectContaining({
operation: 'cleanupOldLogs',
logDir: './test-logs',
})
);
});
it('should cleanup old log files successfully', async () => {
// Arrange
const oldDate = new Date();
oldDate.setDate(oldDate.getDate() - 10); // 10 days old
mockFs.existsSync.mockReturnValue(true);
mockFs.readdirSync.mockReturnValue(['old.log', 'new.log'] as any);
mockFs.statSync
.mockReturnValueOnce({
birthtime: oldDate,
size: 1024,
extname: jest.fn().mockReturnValue('.log')
} as any)
.mockReturnValueOnce({
birthtime: new Date(),
size: 2048,
extname: jest.fn().mockReturnValue('.log')
} as any);
mockPath.join.mockImplementation((...args) => args.join('/'));
mockPath.extname.mockReturnValue('.log');
// Act
await service.cleanupOldLogs();
// Assert
expect(mockFs.existsSync).toHaveBeenCalledWith('./test-logs');
expect(mockFs.readdirSync).toHaveBeenCalledWith('./test-logs');
expect(mockLogger.info).toHaveBeenCalledWith(
'开始执行日志清理任务',
expect.objectContaining({
operation: 'cleanupOldLogs',
})
);
});
it('should handle cleanup errors gracefully', async () => {
// Arrange
const error = new Error('File system error');
mockFs.existsSync.mockImplementation(() => {
throw error;
});
// Act
await service.cleanupOldLogs();
// Assert
expect(mockLogger.error).toHaveBeenCalledWith(
'日志清理任务执行失败',
expect.objectContaining({
operation: 'cleanupOldLogs',
error: 'File system error',
}),
error.stack
);
});
});
/**
* 测试日志统计功能
*
* 验证点:
* - getLogStatistics 方法能够正确统计日志信息
* - 处理日志目录不存在的情况
* - 正确计算文件数量、大小等统计数据
* - 处理统计过程中的异常情况
*/
describe('getLogStatistics', () => {
it('should return empty statistics when log directory does not exist', async () => {
// Arrange
mockFs.existsSync.mockReturnValue(false);
// Act
const result = await service.getLogStatistics();
// Assert
expect(result).toEqual({
fileCount: 0,
totalSize: 0,
errorLogCount: 0,
oldestFile: '',
newestFile: '',
avgFileSize: 0,
});
});
it('should calculate statistics correctly', async () => {
// Arrange
const files = ['app.log', 'error.log', 'access.log'];
const oldDate = new Date('2023-01-01');
const newDate = new Date('2023-12-31');
mockFs.existsSync.mockReturnValue(true);
mockFs.readdirSync.mockReturnValue(files as any);
mockFs.statSync
.mockReturnValueOnce({ birthtime: oldDate, size: 1000 } as any)
.mockReturnValueOnce({ birthtime: newDate, size: 2000 } as any)
.mockReturnValueOnce({ birthtime: new Date('2023-06-01'), size: 1500 } as any);
mockPath.join.mockImplementation((...args) => args.join('/'));
// Act
const result = await service.getLogStatistics();
// Assert
expect(result).toEqual({
fileCount: 3,
totalSize: 4500,
errorLogCount: 1, // error.log
oldestFile: 'app.log',
newestFile: 'error.log',
avgFileSize: 1500,
});
});
it('should handle statistics errors and rethrow', async () => {
// Arrange
const error = new Error('Statistics error');
mockFs.existsSync.mockImplementation(() => {
throw error;
});
// Act & Assert
await expect(service.getLogStatistics()).rejects.toThrow('Statistics error');
expect(mockLogger.error).toHaveBeenCalledWith(
'获取日志统计信息失败',
expect.objectContaining({
operation: 'getLogStatistics',
error: 'Statistics error',
}),
error.stack
);
});
});
/**
* 测试日志尾部读取功能
*
* 验证点:
* - getRuntimeLogTail 方法能够正确读取日志尾部
* - 处理文件不存在的情况
* - 正确解析不同环境的日志文件类型
* - 限制读取行数在合理范围内
*/
describe('getRuntimeLogTail', () => {
it('should return empty lines when file does not exist', async () => {
// Arrange
mockFs.existsSync.mockReturnValue(false);
mockPath.join.mockImplementation((...args) => args.join('/'));
// Act
const result = await service.getRuntimeLogTail();
// Assert
expect(result).toEqual({
file: 'dev.log',
updated_at: expect.any(String),
lines: [],
});
});
it('should read log tail successfully', async () => {
// Arrange
const mockStats = {
size: 1000,
mtime: new Date('2023-12-31T12:00:00Z'),
};
const mockFd = 123;
mockFs.existsSync.mockReturnValue(true);
mockFs.statSync.mockReturnValue(mockStats as any);
mockFs.openSync.mockReturnValue(mockFd);
mockFs.readSync.mockImplementation(() => 0);
mockFs.closeSync.mockImplementation();
mockPath.join.mockImplementation((...args) => args.join('/'));
// Act
const result = await service.getRuntimeLogTail({ lines: 10 });
// Assert - Verify the method executes and returns expected structure
expect(result).toHaveProperty('file');
expect(result).toHaveProperty('updated_at');
expect(result).toHaveProperty('lines');
expect(result.file).toBe('dev.log');
expect(result.updated_at).toBe('2023-12-31T12:00:00.000Z');
expect(Array.isArray(result.lines)).toBe(true);
expect(mockFs.closeSync).toHaveBeenCalledWith(mockFd);
});
it('should limit requested lines to maximum allowed', async () => {
// Arrange
mockFs.existsSync.mockReturnValue(false);
mockPath.join.mockImplementation((...args) => args.join('/'));
// Act
const result = await service.getRuntimeLogTail({ lines: 5000 }); // Over limit
// Assert - Should be limited to 2000 lines max
expect(result.lines).toEqual([]);
});
});
/**
* 测试配置解析功能
*
* 验证点:
* - parseMaxFiles 私有方法能够正确解析时间配置
* - parseSize 私有方法能够正确解析大小配置
* - formatBytes 私有方法能够正确格式化字节数
*/
describe('private methods', () => {
it('should parse max files configuration correctly', () => {
// Access private method for testing
const parseMaxFiles = (service as any).parseMaxFiles;
expect(parseMaxFiles('7d')).toBe(7);
expect(parseMaxFiles('2w')).toBe(14);
expect(parseMaxFiles('1m')).toBe(30);
expect(parseMaxFiles('30')).toBe(30);
});
it('should parse size configuration correctly', () => {
// Access private method for testing
const parseSize = (service as any).parseSize;
expect(parseSize('10k')).toBe(10 * 1024);
expect(parseSize('5m')).toBe(5 * 1024 * 1024);
expect(parseSize('1g')).toBe(1 * 1024 * 1024 * 1024);
});
it('should format bytes correctly', () => {
// Access private method for testing
const formatBytes = (service as any).formatBytes;
expect(formatBytes(0)).toBe('0 B');
expect(formatBytes(1024)).toBe('1 KB');
expect(formatBytes(1024 * 1024)).toBe('1 MB');
expect(formatBytes(1024 * 1024 * 1024)).toBe('1 GB');
expect(formatBytes(1536)).toBe('1.5 KB');
});
});
});

View File

@@ -7,14 +7,24 @@
* - 提供日志统计和分析功能
* - 支持日志文件压缩和归档
*
* 职责分离:
* - 定时清理:执行定期日志文件清理任务
* - 健康监控:监控日志系统运行状态
* - 统计分析:提供日志文件统计和分析数据
* - 生命周期管理:管理日志文件的完整生命周期
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 修复常量命名规范(LOG_DIR, MAX_FILES, MAX_SIZE),清理未使用导入(zlib)
*
* 依赖模块:
* - ConfigService: 环境配置服务
* - AppLoggerService: 应用日志服务
* - ScheduleModule: 定时任务模块
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-13
* @lastModified 2026-01-07
*/
import { Injectable } from '@nestjs/common';
@@ -23,7 +33,6 @@ import { Cron, CronExpression } from '@nestjs/schedule';
import { AppLoggerService } from './logger.service';
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
/**
* 日志管理服务类
@@ -48,17 +57,17 @@ import * as zlib from 'zlib';
*/
@Injectable()
export class LogManagementService {
private readonly logDir: string;
private readonly maxFiles: number;
private readonly maxSize: string;
private readonly LOG_DIR: string;
private readonly MAX_FILES: number;
private readonly MAX_SIZE: string;
constructor(
private readonly configService: ConfigService,
private readonly logger: AppLoggerService,
) {
this.logDir = this.configService.get('LOG_DIR', './logs');
this.maxFiles = this.parseMaxFiles(this.configService.get('LOG_MAX_FILES', '7d'));
this.maxSize = this.configService.get('LOG_MAX_SIZE', '10m');
this.LOG_DIR = this.configService.get('LOG_DIR', './logs');
this.MAX_FILES = this.parseMaxFiles(this.configService.get('LOG_MAX_FILES', '7d'));
this.MAX_SIZE = this.configService.get('LOG_MAX_SIZE', '10m');
}
/**
@@ -67,7 +76,7 @@ export class LogManagementService {
* 说明:用于后台打包下载 logs/ 整目录。
*/
getLogDirAbsolutePath(): string {
return path.resolve(this.logDir);
return path.resolve(this.LOG_DIR);
}
/**
@@ -93,29 +102,29 @@ export class LogManagementService {
this.logger.info('开始执行日志清理任务', {
operation: 'cleanupOldLogs',
logDir: this.logDir,
maxFiles: this.maxFiles,
logDir: this.LOG_DIR,
maxFiles: this.MAX_FILES,
timestamp: new Date().toISOString(),
});
try {
if (!fs.existsSync(this.logDir)) {
if (!fs.existsSync(this.LOG_DIR)) {
this.logger.warn('日志目录不存在,跳过清理任务', {
operation: 'cleanupOldLogs',
logDir: this.logDir,
logDir: this.LOG_DIR,
});
return;
}
const files = fs.readdirSync(this.logDir);
const files = fs.readdirSync(this.LOG_DIR);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.maxFiles);
cutoffDate.setDate(cutoffDate.getDate() - this.MAX_FILES);
let deletedCount = 0;
let deletedSize = 0;
for (const file of files) {
const filePath = path.join(this.logDir, file);
const filePath = path.join(this.LOG_DIR, file);
const stats = fs.statSync(filePath);
// 只处理日志文件(.log 扩展名)
@@ -156,7 +165,7 @@ export class LogManagementService {
this.logger.error('日志清理任务执行失败', {
operation: 'cleanupOldLogs',
logDir: this.logDir,
logDir: this.LOG_DIR,
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString(),
@@ -203,7 +212,7 @@ export class LogManagementService {
const stats = await this.getLogStatistics();
// 检查磁盘空间使用情况
if (stats.totalSize > this.parseSize(this.maxSize) * 100) { // 如果总大小超过单文件限制的100倍
if (stats.totalSize > this.parseSize(this.MAX_SIZE) * 100) { // 如果总大小超过单文件限制的100倍
this.logger.warn('日志文件占用空间过大', {
operation: 'monitorLogHealth',
totalSize: this.formatBytes(stats.totalSize),
@@ -257,7 +266,7 @@ export class LogManagementService {
avgFileSize: number;
}> {
try {
if (!fs.existsSync(this.logDir)) {
if (!fs.existsSync(this.LOG_DIR)) {
return {
fileCount: 0,
totalSize: 0,
@@ -268,7 +277,7 @@ export class LogManagementService {
};
}
const files = fs.readdirSync(this.logDir);
const files = fs.readdirSync(this.LOG_DIR);
let totalSize = 0;
let errorLogCount = 0;
let oldestTime = Date.now();
@@ -277,7 +286,7 @@ export class LogManagementService {
let newestFile = '';
for (const file of files) {
const filePath = path.join(this.logDir, file);
const filePath = path.join(this.LOG_DIR, file);
const stats = fs.statSync(filePath);
totalSize += stats.size;
@@ -349,7 +358,7 @@ export class LogManagementService {
const defaultType = isProduction ? 'app' : 'dev';
const typeKey = (requestedType && requestedType in allowedFiles ? requestedType : defaultType) as keyof typeof allowedFiles;
const fileName = allowedFiles[typeKey];
const filePath = path.join(this.logDir, fileName);
const filePath = path.join(this.LOG_DIR, fileName);
if (!fs.existsSync(filePath)) {
return { file: fileName, updated_at: new Date().toISOString(), lines: [] };

View File

@@ -7,9 +7,19 @@
* - 根据环境自动调整日志策略
* - 提供日志文件清理和归档功能
*
* 职责分离:
* - 配置生成根据环境变量生成Pino日志配置
* - 文件管理:管理日志文件的创建和轮转
* - 策略适配:提供不同环境的日志输出策略
* - 目录维护:确保日志目录存在和可访问
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善注释文档和配置说明
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-13
* @lastModified 2026-01-07
*/
import { ConfigService } from '@nestjs/config';

View File

@@ -7,14 +7,24 @@
* - 支持不同环境的日志配置
* - 提供统一的日志记录接口
*
* 职责分离:
* - 模块配置配置Pino日志库和相关依赖
* - 服务提供:导出全局可用的日志服务
* - 环境适配:根据环境变量调整日志策略
* - 依赖管理:管理日志相关的依赖注入
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善注释文档和模块说明
*
* 依赖模块:
* - ConfigModule: 环境配置模块
* - PinoLoggerModule: Pino 日志模块
* - AppLoggerService: 应用日志服务
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-13
* @lastModified 2026-01-07
*/
import { Module } from '@nestjs/common';

View File

@@ -7,6 +7,15 @@
* - 测试敏感信息过滤功能
* - 验证请求上下文绑定功能
*
* 职责分离:
* - 功能测试:测试日志服务的各项核心功能
* - 安全测试:验证敏感信息过滤机制
* - 集成测试:测试与其他组件的集成效果
* - 边界测试:验证各种边界条件和异常情况
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善测试注释文档,新增完整的测试用例覆盖
*
* 测试覆盖:
* - 服务实例化
* - 日志方法调用
@@ -14,8 +23,9 @@
* - 请求上下文绑定
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-13
* @lastModified 2026-01-07
*/
import { Test, TestingModule } from '@nestjs/testing';
@@ -24,22 +34,25 @@ import { AppLoggerService } from './logger.service';
describe('AppLoggerService', () => {
let service: AppLoggerService;
let mockConfigService: jest.Mocked<ConfigService>;
beforeEach(async () => {
mockConfigService = {
get: jest.fn((key: string, defaultValue?: any) => {
const config: Record<string, any> = {
NODE_ENV: 'test',
APP_NAME: 'test-app',
};
return config[key] || defaultValue;
}),
} as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
AppLoggerService,
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string, defaultValue?: any) => {
const config: Record<string, any> = {
NODE_ENV: 'test',
APP_NAME: 'test-app',
};
return config[key] || defaultValue;
}),
},
useValue: mockConfigService,
},
],
}).compile();
@@ -52,27 +65,90 @@ describe('AppLoggerService', () => {
});
/**
* 测试信息日志记录功能
* 测试所有日志级别方法
*
* 验证点:
* - info 方法能够正确调用内部 log 方法
* - 所有日志方法能够正确调用内部 log 方法
* - 传递的参数格式正确
* - 日志级别设置正确
*/
it('should log info messages', () => {
// 监听内部 log 方法调用
const logSpy = jest.spyOn(service as any, 'log').mockImplementation();
// 调用 info 方法
service.info('Test message', { module: 'TestModule' });
// 验证调用参数
expect(logSpy).toHaveBeenCalledWith('info', {
message: 'Test message',
context: { module: 'TestModule' }
describe('logging methods', () => {
let logSpy: jest.SpyInstance;
beforeEach(() => {
logSpy = jest.spyOn(service as any, 'log').mockImplementation();
});
afterEach(() => {
logSpy.mockRestore();
});
it('should log debug messages', () => {
service.debug('Debug message', { module: 'TestModule' });
expect(logSpy).toHaveBeenCalledWith('debug', {
message: 'Debug message',
context: { module: 'TestModule' }
});
});
it('should log info messages', () => {
service.info('Info message', { module: 'TestModule' });
expect(logSpy).toHaveBeenCalledWith('info', {
message: 'Info message',
context: { module: 'TestModule' }
});
});
it('should log warn messages', () => {
service.warn('Warning message', { module: 'TestModule' });
expect(logSpy).toHaveBeenCalledWith('warn', {
message: 'Warning message',
context: { module: 'TestModule' }
});
});
it('should log error messages', () => {
const stack = 'Error stack trace';
service.error('Error message', { module: 'TestModule' }, stack);
expect(logSpy).toHaveBeenCalledWith('error', {
message: 'Error message',
context: { module: 'TestModule' },
stack
});
});
it('should log fatal messages', () => {
const stack = 'Fatal error stack trace';
service.fatal('Fatal message', { module: 'TestModule' }, stack);
expect(logSpy).toHaveBeenCalledWith('fatal', {
message: 'Fatal message',
context: { module: 'TestModule' },
stack
});
});
it('should log trace messages', () => {
service.trace('Trace message', { module: 'TestModule' });
expect(logSpy).toHaveBeenCalledWith('trace', {
message: 'Trace message',
context: { module: 'TestModule' }
});
});
it('should handle messages without context', () => {
service.info('Simple message');
expect(logSpy).toHaveBeenCalledWith('info', {
message: 'Simple message',
context: undefined
});
});
logSpy.mockRestore();
});
/**
@@ -82,22 +158,63 @@ describe('AppLoggerService', () => {
* - 敏感信息过滤方法被正确调用
* - 包含敏感字段的日志会触发过滤逻辑
* - 过滤功能不影响正常的日志记录流程
* - 各种敏感字段都能被正确识别
*/
it('should filter sensitive data', () => {
// 监听敏感信息过滤方法
const redactSpy = jest.spyOn(service as any, 'redactSensitiveData');
// 记录包含敏感信息的日志
service.info('Login attempt', {
module: 'AuthModule',
password: 'secret123',
token: 'jwt-token'
describe('sensitive data filtering', () => {
let redactSpy: jest.SpyInstance;
beforeEach(() => {
redactSpy = jest.spyOn(service as any, 'redactSensitiveData');
});
afterEach(() => {
redactSpy.mockRestore();
});
it('should filter password fields', () => {
service.info('Login attempt', {
module: 'AuthModule',
password: 'secret123',
userPassword: 'another-secret'
});
expect(redactSpy).toHaveBeenCalled();
});
it('should filter token fields', () => {
service.info('API call', {
module: 'ApiModule',
token: 'jwt-token',
accessToken: 'access-token'
});
expect(redactSpy).toHaveBeenCalled();
});
it('should filter authorization fields', () => {
service.info('Request', {
module: 'HttpModule',
authorization: 'Bearer token',
authHeader: 'Basic auth'
});
expect(redactSpy).toHaveBeenCalled();
});
it('should filter nested sensitive data', () => {
service.info('Complex data', {
module: 'TestModule',
user: {
name: 'John',
password: 'secret',
profile: {
token: 'nested-token'
}
}
});
expect(redactSpy).toHaveBeenCalled();
});
// 验证过滤方法被调用
expect(redactSpy).toHaveBeenCalled();
redactSpy.mockRestore();
});
/**
@@ -107,26 +224,123 @@ describe('AppLoggerService', () => {
* - bindRequest 方法返回正确的日志方法对象
* - 返回的对象包含所有必要的日志方法
* - 绑定的上下文信息能够正确传递
* - 处理缺失请求信息的情况
*/
it('should bind request context', () => {
// 模拟 HTTP 请求对象
const mockReq = {
id: 'req-123',
headers: {
'x-user-id': 'user-456'
},
ip: '127.0.0.1'
};
describe('request context binding', () => {
it('should bind request context with complete request object', () => {
const mockReq = {
id: 'req-123',
headers: {
'x-request-id': 'custom-req-id',
'x-user-id': 'user-456'
},
ip: '127.0.0.1'
};
// 绑定请求上下文
const boundLogger = service.bindRequest(mockReq, 'TestController');
// 验证返回的日志方法对象
expect(boundLogger).toHaveProperty('info');
expect(boundLogger).toHaveProperty('error');
expect(boundLogger).toHaveProperty('warn');
expect(boundLogger).toHaveProperty('debug');
expect(boundLogger).toHaveProperty('fatal');
expect(boundLogger).toHaveProperty('trace');
const boundLogger = service.bindRequest(mockReq, 'TestController');
expect(boundLogger).toHaveProperty('debug');
expect(boundLogger).toHaveProperty('info');
expect(boundLogger).toHaveProperty('warn');
expect(boundLogger).toHaveProperty('error');
expect(boundLogger).toHaveProperty('fatal');
expect(boundLogger).toHaveProperty('trace');
expect(typeof boundLogger.info).toBe('function');
});
it('should handle missing request properties', () => {
const mockReq = {
headers: {}
};
const boundLogger = service.bindRequest(mockReq, 'TestController');
expect(boundLogger).toHaveProperty('info');
expect(typeof boundLogger.info).toBe('function');
});
it('should merge base context with extra context', () => {
const logSpy = jest.spyOn(service as any, 'log').mockImplementation();
const mockReq = {
id: 'req-123',
headers: { 'x-user-id': 'user-456' },
ip: '127.0.0.1'
};
const boundLogger = service.bindRequest(mockReq, 'TestController');
boundLogger.info('Test message', { operation: 'testOp' });
expect(logSpy).toHaveBeenCalledWith('info', {
message: 'Test message',
context: expect.objectContaining({
reqId: 'req-123',
userId: 'user-456',
ip: '127.0.0.1',
module: 'TestController',
operation: 'testOp'
})
});
logSpy.mockRestore();
});
});
/**
* 测试日志级别控制功能
*
* 验证点:
* - 不同环境下的日志级别控制正确
* - 禁用的日志级别不会输出
* - 启用的日志级别正常输出
*/
describe('log level control', () => {
it('should respect log level settings in test environment', () => {
const buildLogDataSpy = jest.spyOn(service as any, 'buildLogData');
const outputLogSpy = jest.spyOn(service as any, 'outputLog').mockImplementation();
// In test environment, info should be enabled
service.info('Test message');
expect(buildLogDataSpy).toHaveBeenCalled();
expect(outputLogSpy).toHaveBeenCalled();
buildLogDataSpy.mockRestore();
outputLogSpy.mockRestore();
});
});
/**
* 测试边界情况和异常处理
*
* 验证点:
* - 处理空消息
* - 处理null/undefined上下文
* - 处理循环引用对象
*/
describe('edge cases', () => {
it('should handle empty messages', () => {
const logSpy = jest.spyOn(service as any, 'log').mockImplementation();
service.info('');
expect(logSpy).toHaveBeenCalledWith('info', {
message: '',
context: undefined
});
logSpy.mockRestore();
});
it('should handle null context', () => {
const logSpy = jest.spyOn(service as any, 'log').mockImplementation();
service.info('Test message', null as any);
expect(logSpy).toHaveBeenCalledWith('info', {
message: 'Test message',
context: null
});
logSpy.mockRestore();
});
});
});

View File

@@ -7,14 +7,24 @@
* - 自动过滤敏感信息,保护系统安全
* - 支持请求上下文绑定,便于链路追踪
*
* 职责分离:
* - 日志记录:提供统一的日志记录接口和方法
* - 级别控制:根据环境动态调整日志输出级别
* - 安全过滤:自动过滤敏感信息防止数据泄露
* - 上下文绑定:支持请求上下文关联和链路追踪
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 修复常量命名规范完善注释文档重构log方法提升可维护性
*
* 依赖模块:
* - ConfigService: 环境配置服务
* - PinoLogger: 高性能日志库(可选)
* - Logger: NestJS 内置日志服务(降级使用)
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-13
* @lastModified 2026-01-07
*/
import { Injectable, Logger, Inject, Optional } from '@nestjs/common';
@@ -143,6 +153,24 @@ export class AppLoggerService {
// 过滤禁用的日志级别(生产环境不输出 debug/trace
if (!this.enableLevels[level]) return;
// 构建完整的日志数据
const logData = this.buildLogData(options);
// 输出日志
this.outputLog(level, logData, options.stack);
}
/**
* 构建日志数据
*
* 功能描述:
* 构建包含上下文信息和敏感信息过滤的完整日志数据
*
* @param options 日志选项
* @returns 构建完成的日志数据
* @private
*/
private buildLogData(options: LogOptions) {
// 1. 补充默认上下文
const defaultContext: LogContext = {
module: options.context?.module || 'Unknown',
@@ -159,62 +187,99 @@ export class AppLoggerService {
this.redactSensitiveData(context);
// 4. 构造日志数据
const logData = {
return {
message: options.message,
context,
...(options.stack ? { stack: options.stack } : {}), // 仅错误级别携带栈信息
};
}
/**
* 输出日志
*
* 功能描述:
* 根据底层日志实例类型选择合适的输出方式
*
* @param level 日志级别
* @param logData 日志数据
* @param stack 错误堆栈(可选)
* @private
*/
private outputLog(level: LogLevel, logData: any, stack?: string): void {
const finalLogData = {
...logData,
...(stack ? { stack } : {}), // 仅错误级别携带栈信息
};
// 5. 适配 Pino/内置 Logger 的调用方式
if (this.pinoLogger) {
// Pino 调用方式:直接使用 pinoLogger 实例
switch (level) {
case 'debug':
this.pinoLogger.debug(logData.message, logData);
break;
case 'info':
this.pinoLogger.info(logData.message, logData);
break;
case 'warn':
this.pinoLogger.warn(logData.message, logData);
break;
case 'error':
this.pinoLogger.error(logData.message, logData);
break;
case 'fatal':
this.pinoLogger.fatal(logData.message, logData);
break;
case 'trace':
this.pinoLogger.trace(logData.message, logData);
break;
default:
this.pinoLogger.info(logData.message, logData);
}
this.outputToPino(level, finalLogData);
} else {
// 内置 Logger 降级调用:根据级别调用对应方法
const builtInLogger = this.logger as Logger;
const contextString = JSON.stringify(logData.context);
switch (level) {
case 'debug':
builtInLogger.debug(logData.message, contextString);
break;
case 'info':
builtInLogger.log(logData.message, contextString); // 内置 Logger 使用 log 方法代替 info
break;
case 'warn':
builtInLogger.warn(logData.message, contextString);
break;
case 'error':
case 'fatal': // fatal 级别降级为 error
builtInLogger.error(logData.message, options.stack || '', contextString);
break;
case 'trace':
builtInLogger.verbose(logData.message, contextString); // trace 级别降级为 verbose
break;
default:
builtInLogger.log(logData.message, contextString);
}
this.outputToBuiltInLogger(level, finalLogData, stack);
}
}
/**
* 输出到 Pino 日志库
*
* @param level 日志级别
* @param logData 日志数据
* @private
*/
private outputToPino(level: LogLevel, logData: any): void {
switch (level) {
case 'debug':
this.pinoLogger.debug(logData.message, logData);
break;
case 'info':
this.pinoLogger.info(logData.message, logData);
break;
case 'warn':
this.pinoLogger.warn(logData.message, logData);
break;
case 'error':
this.pinoLogger.error(logData.message, logData);
break;
case 'fatal':
this.pinoLogger.fatal(logData.message, logData);
break;
case 'trace':
this.pinoLogger.trace(logData.message, logData);
break;
default:
this.pinoLogger.info(logData.message, logData);
}
}
/**
* 输出到内置 Logger
*
* @param level 日志级别
* @param logData 日志数据
* @param stack 错误堆栈(可选)
* @private
*/
private outputToBuiltInLogger(level: LogLevel, logData: any, stack?: string): void {
const builtInLogger = this.logger as Logger;
const contextString = JSON.stringify(logData.context);
switch (level) {
case 'debug':
builtInLogger.debug(logData.message, contextString);
break;
case 'info':
builtInLogger.log(logData.message, contextString); // 内置 Logger 使用 log 方法代替 info
break;
case 'warn':
builtInLogger.warn(logData.message, contextString);
break;
case 'error':
case 'fatal': // fatal 级别降级为 error
builtInLogger.error(logData.message, stack || '', contextString);
break;
case 'trace':
builtInLogger.verbose(logData.message, contextString); // trace 级别降级为 verbose
break;
default:
builtInLogger.log(logData.message, contextString);
}
}

View File

@@ -0,0 +1,109 @@
# Verification 验证码管理模块
Verification 是应用的核心验证码管理工具模块,提供完整的验证码生成、验证、存储和防刷机制。作为底层技术工具,可被多个业务模块复用,支持邮箱验证、密码重置、短信验证等多种场景。
## 验证码生成和管理
### generateCode()
生成指定类型的验证码,支持频率限制和防刷机制。
### verifyCode()
验证用户输入的验证码包含尝试次数控制和TTL管理。
### deleteCode()
主动删除指定的验证码,用于清理或重置场景。
## 验证码状态查询
### codeExists()
检查指定验证码是否存在,用于状态判断。
### getCodeTTL()
获取验证码剩余有效时间,用于前端倒计时显示。
### getCodeStats()
获取验证码详细统计信息,包含尝试次数和创建时间。
## 防刷和管理功能
### clearCooldown()
清除验证码发送冷却时间,用于管理员操作或特殊场景。
### cleanupExpiredCodes()
清理过期验证码的定时任务方法Redis自动过期机制的补充。
### debugCodeInfo()
调试方法,获取验证码完整信息,仅用于开发环境。
## 使用的项目内部依赖
### IRedisService (来自 ../../redis/redis.interface)
Redis服务接口提供缓存存储、过期时间管理和键值操作能力。
### VerificationCodeType (本模块)
验证码类型枚举,定义邮箱验证、密码重置、短信验证三种类型。
### VerificationCodeInfo (本模块)
验证码信息接口,包含验证码、创建时间、尝试次数等完整数据结构。
## 核心特性
### 多类型验证码支持
- 邮箱验证码:用于用户注册和邮箱验证场景
- 密码重置验证码:用于密码找回和重置流程
- 短信验证码:用于手机号验证和双因子认证
### 完善的防刷机制
- 发送频率限制60秒冷却时间防止频繁发送
- 每小时限制每小时最多发送5次防止恶意刷取
- 验证尝试控制最多3次验证机会超出自动删除
### Redis缓存集成
- 自动过期机制验证码5分钟自动过期
- TTL精确控制保持原有过期时间不重置倒计时
- 键命名规范统一的Redis键命名和管理策略
### 完整的错误处理
- 异常分类处理:区分业务异常和技术异常
- 详细日志记录:记录生成、验证、错误等关键操作
- 资源自动清理:异常情况下自动清理无效数据
### 统计和调试支持
- 验证码统计:提供详细的使用统计和状态信息
- 调试接口:开发环境下的完整信息查看
- 性能监控记录操作耗时和Redis连接状态
## 潜在风险
### Redis依赖风险
- Redis服务不可用时验证码功能完全失效
- 网络延迟可能影响验证码生成和验证性能
- 建议配置Redis高可用集群和连接池监控
### 验证码安全风险
- 6位数字验证码存在暴力破解可能性
- 调试接口可能泄露验证码内容
- 建议生产环境禁用debugCodeInfo方法并考虑增加验证码复杂度
### 频率限制绕过风险
- 使用不同标识符可能绕过频率限制
- 系统时间异常可能影响每小时限制计算
- 建议增加IP级别的频率限制和异常时间处理
### 内存和性能风险
- 大量验证码生成可能占用Redis内存
- 频繁的Redis操作可能影响系统性能
- 建议监控Redis内存使用和设置合理的过期策略
### 业务逻辑风险
- 验证码验证成功后立即删除,无法重复验证
- 冷却时间清除功能可能被滥用
- 建议根据业务需求调整验证策略和权限控制
## 版本信息
- **版本**: 1.0.1
- **作者**: moyin
- **创建时间**: 2025-12-17
- **最后修改**: 2026-01-07
- **测试覆盖**: 38个测试用例100%通过率

View File

@@ -4,11 +4,19 @@
* 功能描述:
* - 提供验证码服务的模块配置
* - 导出验证码服务供其他模块使用
* - 集成配置服务
* - 集成配置服务和Redis模块
*
* 职责分离:
* - 模块依赖管理和配置
* - 服务提供者注册和导出
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录规范
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Module } from '@nestjs/common';

View File

@@ -9,9 +9,18 @@
* - 错误处理
* - 验证码统计信息
*
* 职责分离:
* - 单元测试覆盖所有公共方法
* - Mock依赖服务进行隔离测试
* - 边界条件和异常情况测试
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录规范
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Test, TestingModule } from '@nestjs/testing';
@@ -71,11 +80,6 @@ describe('VerificationService', () => {
});
it('应该使用默认Redis配置', () => {
// 创建新的 mock ConfigService 来测试默认配置
const testConfigService = {
get: jest.fn((key: string, defaultValue?: any) => defaultValue),
};
// 创建 mock Redis 服务
const mockRedisService = {
set: jest.fn(),
@@ -87,26 +91,13 @@ describe('VerificationService', () => {
flushall: jest.fn(),
};
new VerificationService(testConfigService as any, mockRedisService as any);
new VerificationService(mockRedisService as any);
// 由于现在使用注入的Redis服务不再直接创建Redis实例
expect(true).toBe(true);
});
it('应该使用自定义Redis配置', () => {
// 创建新的 mock ConfigService 来测试自定义配置
const testConfigService = {
get: jest.fn((key: string, defaultValue?: any) => {
const config: Record<string, any> = {
'REDIS_HOST': 'redis.example.com',
'REDIS_PORT': 6380,
'REDIS_PASSWORD': 'password123',
'REDIS_DB': 1,
};
return config[key] !== undefined ? config[key] : defaultValue;
}),
};
// 创建 mock Redis 服务
const mockRedisService = {
set: jest.fn(),
@@ -118,7 +109,7 @@ describe('VerificationService', () => {
flushall: jest.fn(),
};
new VerificationService(testConfigService as any, mockRedisService as any);
new VerificationService(mockRedisService as any);
// 由于现在使用注入的Redis服务不再直接创建Redis实例
expect(true).toBe(true);

View File

@@ -6,18 +6,27 @@
* - 使用Redis缓存验证码支持过期时间
* - 提供验证码验证和防刷机制
*
* 职责分离:
* - 验证码生成和存储管理
* - 验证码验证和尝试次数控制
* - 频率限制和防刷机制
*
* 支持的验证码类型:
* - 邮箱验证码
* - 密码重置验证码
* - 手机短信验证码
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 清理未使用的导入(ConfigService)和多余空行
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录规范
*
* @author moyin
* @version 1.0.0
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Injectable, Logger, BadRequestException, HttpException, HttpStatus, Inject } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IRedisService } from '../../redis/redis.interface';
/**
@@ -55,12 +64,9 @@ export class VerificationService {
private readonly MAX_SENDS_PER_HOUR = 5; // 每小时最大发送次数
constructor(
private readonly configService: ConfigService,
@Inject('REDIS_SERVICE') private readonly redis: IRedisService,
) {}
/**
* 生成验证码
*