Files
whale-town-end/src/core/utils/email/email.service.ts
moyin bb796a2469 refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case  snake_case)
- 重构zulip模块为zulip_core,明确Core层职责
- 重构user-mgmt模块为user_mgmt,统一命名规范
- 调整模块依赖关系,优化架构分层
- 删除过时的文件和目录结构
- 更新相关文档和配置文件

本次重构涉及大量文件重命名和模块重组,
旨在建立更清晰的项目架构和统一的命名规范。
2026-01-08 00:14:14 +08:00

596 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 邮件服务
*
* 功能描述:
* - 提供邮件发送的核心功能
* - 支持多种邮件模板和场景
* - 集成主流邮件服务提供商
*
* 支持的邮件类型:
* - 邮箱验证码
* - 密码重置验证码
* - 欢迎邮件
* - 系统通知
*
* 职责分离:
* - 邮件发送:核心邮件发送功能实现
* - 模板管理:各种邮件模板的生成和管理
* - 配置管理:邮件服务配置和连接管理
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 清理未使用的导入(BadRequestException),移除多余注释
* - 2026-01-07: 代码规范优化 - 完善方法注释和修改记录
*
* @author moyin
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
import { Transporter } from 'nodemailer';
/**
* 邮件发送选项接口
*/
export interface EmailOptions {
/** 收件人邮箱 */
to: string;
/** 邮件主题 */
subject: string;
/** 邮件内容HTML格式 */
html: string;
/** 邮件内容(纯文本格式) */
text?: string;
}
/**
* 验证码邮件选项接口
*/
export interface VerificationEmailOptions {
/** 收件人邮箱 */
email: string;
/** 验证码 */
code: string;
/** 用户昵称 */
nickname?: string;
/** 验证码用途 */
purpose: 'email_verification' | 'password_reset' | 'login_verification';
}
/**
* 邮件发送结果接口
*/
export interface EmailSendResult {
/** 是否成功 */
success: boolean;
/** 是否为测试模式 */
isTestMode: boolean;
/** 错误信息(如果失败) */
error?: string;
}
/**
* 邮件服务类
*
* 职责:
* - 邮件发送功能:提供统一的邮件发送接口
* - 模板管理:管理各种邮件模板(验证码、欢迎邮件等)
* - 配置管理:处理邮件服务配置和连接
* - 测试模式:支持开发环境的邮件测试模式
*
* 主要方法:
* - sendEmail() - 通用邮件发送方法
* - sendVerificationCode() - 发送验证码邮件
* - sendWelcomeEmail() - 发送欢迎邮件
* - verifyConnection() - 验证邮件服务连接
*
* 使用场景:
* - 用户注册时发送邮箱验证码
* - 密码重置时发送重置验证码
* - 用户注册成功后发送欢迎邮件
* - 登录验证时发送登录验证码
*/
@Injectable()
export class EmailService {
private readonly logger = new Logger(EmailService.name);
private transporter: Transporter;
constructor(private readonly configService: ConfigService) {
this.initializeTransporter();
}
/**
* 初始化邮件传输器
*
* 业务逻辑:
* 1. 从配置服务获取邮件服务配置(主机、端口、安全设置、认证信息)
* 2. 检查是否配置了用户名和密码
* 3. 未配置创建测试模式传输器streamTransport
* 4. 已配置创建真实SMTP传输器
* 5. 记录初始化结果到日志
* 6. 设置transporter实例
*/
private initializeTransporter(): void {
const emailConfig = {
host: this.configService.get<string>('EMAIL_HOST', 'smtp.gmail.com'),
port: this.configService.get<number>('EMAIL_PORT', 587),
secure: this.configService.get<boolean>('EMAIL_SECURE', false), // true for 465, false for other ports
auth: {
user: this.configService.get<string>('EMAIL_USER'),
pass: this.configService.get<string>('EMAIL_PASS'),
},
};
// 如果没有配置邮件服务,使用测试模式
if (!emailConfig.auth.user || !emailConfig.auth.pass) {
this.logger.warn('邮件服务未配置,将使用测试模式(邮件不会真实发送)');
this.transporter = nodemailer.createTransport({
streamTransport: true,
newline: 'unix',
buffer: true
});
} else {
this.transporter = nodemailer.createTransport(emailConfig);
this.logger.log('邮件服务初始化成功');
}
}
/**
* 检查是否为测试模式
*
* 业务逻辑:
* 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;
}
/**
* 发送邮件
*
* 业务逻辑:
* 1. 构建邮件选项(发件人、收件人、主题、内容)
* 2. 检查是否为测试模式
* 3. 测试模式:输出邮件内容到控制台,不真实发送
* 4. 生产模式通过SMTP服务器发送邮件
* 5. 记录发送结果和错误信息
* 6. 返回发送结果状态
*
* @param options 邮件选项
* @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 {
const mailOptions = {
from: this.configService.get<string>('EMAIL_FROM', '"Whale Town Game" <noreply@whaletown.com>'),
to: options.to,
subject: options.subject,
html: options.html,
text: options.text,
};
const isTestMode = this.isTestMode();
// 如果是测试模式,输出邮件内容到控制台
if (isTestMode) {
this.logger.warn('=== 邮件发送(测试模式 - 邮件未真实发送) ===');
this.logger.warn(`收件人: ${options.to}`);
this.logger.warn(`主题: ${options.subject}`);
this.logger.warn(`内容: ${options.text || '请查看HTML内容'}`);
this.logger.warn('⚠️ 注意: 这是测试模式,邮件不会真实发送到用户邮箱');
this.logger.warn('💡 提示: 请在 .env 文件中配置邮件服务以启用真实发送');
this.logger.warn('================================================');
return { success: true, isTestMode: true };
}
// 真实发送邮件
const result = await this.transporter.sendMail(mailOptions);
this.logger.log(`✅ 邮件发送成功: ${options.to}`);
return { success: true, isTestMode: false };
} catch (error) {
this.logger.error(`❌ 邮件发送失败: ${options.to}`, error instanceof Error ? error.stack : String(error));
return {
success: false,
isTestMode: this.isTestMode(),
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* 发送邮箱验证码
*
* 业务逻辑:
* 1. 根据验证码用途选择对应的邮件主题和模板
* 2. 邮箱验证:使用邮箱验证模板
* 3. 密码重置:使用密码重置模板
* 4. 登录验证:使用登录验证模板
* 5. 生成HTML邮件内容和纯文本内容
* 6. 调用sendEmail方法发送邮件
* 7. 返回发送结果
*
* @param options 验证码邮件选项
* @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;
let subject: string;
let template: string;
if (purpose === 'email_verification') {
subject = '【Whale Town】邮箱验证码';
template = this.getEmailVerificationTemplate(code, nickname);
} else if (purpose === 'password_reset') {
subject = '【Whale Town】密码重置验证码';
template = this.getPasswordResetTemplate(code, nickname);
} else if (purpose === 'login_verification') {
subject = '【Whale Town】登录验证码';
template = this.getLoginVerificationTemplate(code, nickname);
} else {
subject = '【Whale Town】验证码';
template = this.getEmailVerificationTemplate(code, nickname);
}
return await this.sendEmail({
to: email,
subject,
html: template,
text: `您的验证码是:${code}5分钟内有效请勿泄露给他人。`
});
}
/**
* 发送欢迎邮件
*
* 业务逻辑:
* 1. 设置欢迎邮件主题
* 2. 生成包含用户昵称的欢迎邮件模板
* 3. 模板包含游戏特色介绍(建造创造、社交互动、任务挑战)
* 4. 调用sendEmail方法发送邮件
* 5. 返回发送结果
*
* @param email 邮箱地址
* @param nickname 用户昵称
* @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';
const template = this.getWelcomeTemplate(nickname);
return await this.sendEmail({
to: email,
subject,
html: template,
text: `欢迎 ${nickname} 加入 Whale Town 像素游戏世界!`
});
}
/**
* 获取邮箱验证模板
*
* @param code 验证码
* @param nickname 用户昵称
* @returns HTML模板
*/
private getEmailVerificationTemplate(code: string, nickname?: string): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>邮箱验证</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; }
.code-box { background: #fff; border: 2px dashed #667eea; padding: 20px; text-align: center; margin: 20px 0; border-radius: 8px; }
.code { font-size: 32px; font-weight: bold; color: #667eea; letter-spacing: 5px; }
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 12px; }
.warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🐋 Whale Town</h1>
<p>邮箱验证</p>
</div>
<div class="content">
<h2>你好${nickname ? ` ${nickname}` : ''}</h2>
<p>感谢您注册 Whale Town 像素游戏!为了确保您的账户安全,请使用以下验证码完成邮箱验证:</p>
<div class="code-box">
<div class="code">${code}</div>
<p style="margin: 10px 0 0 0; color: #666;">验证码</p>
</div>
<div class="warning">
<strong>⚠️ 安全提醒:</strong>
<ul style="margin: 10px 0 0 20px;">
<li>验证码 5 分钟内有效</li>
<li>请勿将验证码泄露给他人</li>
<li>如非本人操作,请忽略此邮件</li>
</ul>
</div>
<p>完成验证后,您就可以开始您的像素世界冒险之旅了!</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿回复</p>
<p>© 2025 Whale Town Game. All rights reserved.</p>
</div>
</div>
</body>
</html>`;
}
/**
* 获取密码重置模板
*
* @param code 验证码
* @param nickname 用户昵称
* @returns HTML模板
*/
private getPasswordResetTemplate(code: string, nickname?: string): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>密码重置</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; }
.code-box { background: #fff; border: 2px dashed #ff6b6b; padding: 20px; text-align: center; margin: 20px 0; border-radius: 8px; }
.code { font-size: 32px; font-weight: bold; color: #ff6b6b; letter-spacing: 5px; }
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 12px; }
.warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 密码重置</h1>
<p>Whale Town 账户安全</p>
</div>
<div class="content">
<h2>你好${nickname ? ` ${nickname}` : ''}</h2>
<p>我们收到了您的密码重置请求。请使用以下验证码来重置您的密码:</p>
<div class="code-box">
<div class="code">${code}</div>
<p style="margin: 10px 0 0 0; color: #666;">密码重置验证码</p>
</div>
<div class="warning">
<strong>🛡️ 安全提醒:</strong>
<ul style="margin: 10px 0 0 20px;">
<li>验证码 5 分钟内有效</li>
<li>请勿将验证码泄露给他人</li>
<li>如非本人操作,请立即联系客服</li>
<li>重置密码后请妥善保管新密码</li>
</ul>
</div>
<p>如果您没有请求重置密码,请忽略此邮件,您的账户仍然安全。</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿回复</p>
<p>© 2025 Whale Town Game. All rights reserved.</p>
</div>
</div>
</body>
</html>`;
}
/**
* 获取登录验证码模板
*
* @param code 验证码
* @param nickname 用户昵称
* @returns HTML模板
*/
private getLoginVerificationTemplate(code: string, nickname?: string): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录验证码</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; }
.code-box { background: #fff; border: 2px dashed #667eea; padding: 20px; text-align: center; margin: 20px 0; border-radius: 8px; }
.code { font-size: 32px; font-weight: bold; color: #667eea; letter-spacing: 5px; }
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 12px; }
.info { background: #e3f2fd; border: 1px solid #bbdefb; padding: 15px; border-radius: 5px; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 登录验证码</h1>
<p>Whale Town 安全登录</p>
</div>
<div class="content">
<h2>你好${nickname ? ` ${nickname}` : ''}</h2>
<p>您正在使用验证码登录 Whale Town。请使用以下验证码完成登录</p>
<div class="code-box">
<div class="code">${code}</div>
<p style="margin: 10px 0 0 0; color: #666;">登录验证码</p>
</div>
<div class="info">
<strong>📱 使用说明:</strong>
<ul style="margin: 10px 0 0 20px;">
<li>验证码 5 分钟内有效</li>
<li>请在登录页面输入此验证码</li>
<li>验证码仅限本次登录使用</li>
<li>请勿将验证码泄露给他人</li>
</ul>
</div>
<p>如果您没有尝试登录,请忽略此邮件,或联系客服确认账户安全。</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿回复</p>
<p>© 2025 Whale Town Game. All rights reserved.</p>
</div>
</div>
</body>
</html>`;
}
/**
* 获取欢迎邮件模板
*
* @param nickname 用户昵称
* @returns HTML模板
*/
private getWelcomeTemplate(nickname: string): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>欢迎加入 Whale Town</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; }
.feature-box { background: #fff; padding: 20px; margin: 15px 0; border-radius: 8px; border-left: 4px solid #4ecdc4; }
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎮 欢迎加入 Whale Town</h1>
<p>像素世界的冒险即将开始</p>
</div>
<div class="content">
<h2>欢迎你,${nickname}</h2>
<p>恭喜您成功注册 Whale Town 像素游戏!您现在已经成为我们像素世界大家庭的一员了。</p>
<div class="feature-box">
<h3>🏗️ 建造与创造</h3>
<p>在像素世界中建造您的梦想家园,发挥无限创意!</p>
</div>
<div class="feature-box">
<h3>🤝 社交互动</h3>
<p>与其他玩家交流互动,结交志同道合的朋友!</p>
</div>
<div class="feature-box">
<h3>🎯 任务挑战</h3>
<p>完成各种有趣的任务,获得丰厚的奖励!</p>
</div>
<p><strong>现在就开始您的像素冒险之旅吧!</strong></p>
<p>如果您在游戏过程中遇到任何问题,随时可以联系我们的客服团队。</p>
</div>
<div class="footer">
<p>祝您游戏愉快!</p>
<p>© 2025 Whale Town Game. All rights reserved.</p>
</div>
</div>
</body>
</html>`;
}
/**
* 验证邮件服务配置
*
* 业务逻辑:
* 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 {
await this.transporter.verify();
this.logger.log('邮件服务连接验证成功');
return true;
} catch (error) {
this.logger.error('邮件服务连接验证失败', error instanceof Error ? error.stack : String(error));
return false;
}
}
}