forked from datawhale/whale-town-end
refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
This commit is contained in:
92
src/core/utils/email/README.md
Normal file
92
src/core/utils/email/README.md
Normal 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)和强密码策略
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
101
src/core/utils/logger/README.md
Normal file
101
src/core/utils/logger/README.md
Normal 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级别,仅在开发环境使用
|
||||
- 大量日志文件可能占用过多磁盘空间,需要定期清理
|
||||
|
||||
### 配置风险
|
||||
- 日志级别配置错误可能导致重要信息丢失或性能问题
|
||||
- 日志目录权限不足可能导致日志写入失败
|
||||
- 建议定期检查日志配置和目录权限
|
||||
|
||||
### 敏感信息泄露风险
|
||||
- 虽然有敏感信息过滤机制,但可能存在遗漏的敏感字段
|
||||
- 建议定期审查敏感字段关键词列表,确保覆盖所有敏感信息
|
||||
- 避免在日志中记录完整的用户输入数据
|
||||
393
src/core/utils/logger/log_management.service.spec.ts
Normal file
393
src/core/utils/logger/log_management.service.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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: [] };
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
109
src/core/utils/verification/README.md
Normal file
109
src/core/utils/verification/README.md
Normal 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%通过率
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user