Files
whale-town-end/src/core/utils/email/email.service.ts
moyin 3e5c171ff6 feat:添加邮件服务
- 实现完整的邮件发送功能
- 支持验证码邮件发送
- 支持欢迎邮件发送
- 集成SMTP配置和Nodemailer
- 添加邮件模板和HTML格式支持
- 包含完整的单元测试
2025-12-17 20:21:11 +08:00

370 lines
12 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.
/**
* 邮件服务
*
* 功能描述:
* - 提供邮件发送的核心功能
* - 支持多种邮件模板和场景
* - 集成主流邮件服务提供商
*
* 支持的邮件类型:
* - 邮箱验证码
* - 密码重置验证码
* - 欢迎邮件
* - 系统通知
*
* @author moyin
* @version 1.0.0
* @since 2025-12-17
*/
import { Injectable, Logger, BadRequestException } 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';
}
@Injectable()
export class EmailService {
private readonly logger = new Logger(EmailService.name);
private transporter: Transporter;
constructor(private readonly configService: ConfigService) {
this.initializeTransporter();
}
/**
* 初始化邮件传输器
*/
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('邮件服务初始化成功');
}
}
/**
* 发送邮件
*
* @param options 邮件选项
* @returns 发送结果
*/
async sendEmail(options: EmailOptions): Promise<boolean> {
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 result = await this.transporter.sendMail(mailOptions);
// 如果是测试模式,输出邮件内容到控制台
if ((this.transporter.options as any).streamTransport) {
this.logger.log('=== 邮件发送(测试模式) ===');
this.logger.log(`收件人: ${options.to}`);
this.logger.log(`主题: ${options.subject}`);
this.logger.log(`内容: ${options.text || '请查看HTML内容'}`);
this.logger.log('========================');
}
this.logger.log(`邮件发送成功: ${options.to}`);
return true;
} catch (error) {
this.logger.error(`邮件发送失败: ${options.to}`, error instanceof Error ? error.stack : String(error));
return false;
}
}
/**
* 发送邮箱验证码
*
* @param options 验证码邮件选项
* @returns 发送结果
*/
async sendVerificationCode(options: VerificationEmailOptions): Promise<boolean> {
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 {
subject = '【Whale Town】密码重置验证码';
template = this.getPasswordResetTemplate(code, nickname);
}
return await this.sendEmail({
to: email,
subject,
html: template,
text: `您的验证码是:${code}5分钟内有效请勿泄露给他人。`
});
}
/**
* 发送欢迎邮件
*
* @param email 邮箱地址
* @param nickname 用户昵称
* @returns 发送结果
*/
async sendWelcomeEmail(email: string, nickname: string): Promise<boolean> {
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 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>`;
}
/**
* 验证邮件服务配置
*
* @returns 验证结果
*/
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;
}
}
}