feat:添加邮件服务
- 实现完整的邮件发送功能 - 支持验证码邮件发送 - 支持欢迎邮件发送 - 集成SMTP配置和Nodemailer - 添加邮件模板和HTML格式支持 - 包含完整的单元测试
This commit is contained in:
23
src/core/utils/email/email.module.ts
Normal file
23
src/core/utils/email/email.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 邮件服务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供邮件服务的模块配置
|
||||
* - 导出邮件服务供其他模块使用
|
||||
* - 集成配置服务
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { EmailService } from './email.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [EmailService],
|
||||
exports: [EmailService],
|
||||
})
|
||||
export class EmailModule {}
|
||||
424
src/core/utils/email/email.service.spec.ts
Normal file
424
src/core/utils/email/email.service.spec.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
/**
|
||||
* 邮件服务测试
|
||||
*
|
||||
* 功能测试:
|
||||
* - 邮件服务初始化
|
||||
* - 邮件发送功能
|
||||
* - 验证码邮件发送
|
||||
* - 欢迎邮件发送
|
||||
* - 邮件模板生成
|
||||
* - 连接验证
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EmailService, EmailOptions, VerificationEmailOptions } from './email.service';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
|
||||
// Mock nodemailer
|
||||
jest.mock('nodemailer');
|
||||
const mockedNodemailer = nodemailer as jest.Mocked<typeof nodemailer>;
|
||||
|
||||
describe('EmailService', () => {
|
||||
let service: EmailService;
|
||||
let configService: jest.Mocked<ConfigService>;
|
||||
let mockTransporter: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
// 创建 mock transporter
|
||||
mockTransporter = {
|
||||
sendMail: jest.fn(),
|
||||
verify: jest.fn(),
|
||||
options: {}
|
||||
};
|
||||
|
||||
// Mock ConfigService
|
||||
const mockConfigService = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock nodemailer.createTransport
|
||||
mockedNodemailer.createTransport.mockReturnValue(mockTransporter);
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EmailService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EmailService>(EmailService);
|
||||
configService = module.get(ConfigService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('初始化测试', () => {
|
||||
it('应该正确初始化邮件服务', () => {
|
||||
expect(service).toBeDefined();
|
||||
expect(mockedNodemailer.createTransport).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在没有配置时使用测试模式', () => {
|
||||
configService.get.mockReturnValue(undefined);
|
||||
|
||||
// 重新创建服务实例来测试测试模式
|
||||
const testService = new EmailService(configService);
|
||||
|
||||
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
||||
streamTransport: true,
|
||||
newline: 'unix',
|
||||
buffer: true
|
||||
});
|
||||
});
|
||||
|
||||
it('应该在有配置时使用真实SMTP', () => {
|
||||
configService.get
|
||||
.mockReturnValueOnce('smtp.gmail.com') // EMAIL_HOST
|
||||
.mockReturnValueOnce(587) // EMAIL_PORT
|
||||
.mockReturnValueOnce(false) // EMAIL_SECURE
|
||||
.mockReturnValueOnce('test@gmail.com') // EMAIL_USER
|
||||
.mockReturnValueOnce('password'); // EMAIL_PASS
|
||||
|
||||
const testService = new EmailService(configService);
|
||||
|
||||
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'test@gmail.com',
|
||||
pass: 'password',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
it('应该成功发送邮件', async () => {
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: '测试邮件',
|
||||
html: '<p>测试内容</p>',
|
||||
text: '测试内容'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
configService.get.mockReturnValue('"Test Sender" <noreply@test.com>');
|
||||
|
||||
const result = await service.sendEmail(emailOptions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith({
|
||||
from: '"Test Sender" <noreply@test.com>',
|
||||
to: 'test@example.com',
|
||||
subject: '测试邮件',
|
||||
html: '<p>测试内容</p>',
|
||||
text: '测试内容',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该在发送失败时返回false', async () => {
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: '测试邮件',
|
||||
html: '<p>测试内容</p>'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('发送失败'));
|
||||
|
||||
const result = await service.sendEmail(emailOptions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('应该在测试模式下输出邮件内容', async () => {
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: '测试邮件',
|
||||
html: '<p>测试内容</p>',
|
||||
text: '测试内容'
|
||||
};
|
||||
|
||||
// Mock transporter with streamTransport option
|
||||
const testTransporter = {
|
||||
...mockTransporter,
|
||||
options: { streamTransport: true },
|
||||
sendMail: jest.fn().mockResolvedValue({ messageId: 'test-id' })
|
||||
};
|
||||
|
||||
// Mock the service to use test transporter
|
||||
const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation();
|
||||
service['transporter'] = testTransporter;
|
||||
|
||||
const result = await service.sendEmail(emailOptions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(loggerSpy).toHaveBeenCalledWith('=== 邮件发送(测试模式) ===');
|
||||
|
||||
loggerSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendVerificationCode', () => {
|
||||
it('应该成功发送邮箱验证码', async () => {
|
||||
const options: VerificationEmailOptions = {
|
||||
email: 'test@example.com',
|
||||
code: '123456',
|
||||
nickname: '测试用户',
|
||||
purpose: 'email_verification'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
configService.get.mockReturnValue('"Test Sender" <noreply@test.com>');
|
||||
|
||||
const result = await service.sendVerificationCode(options);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: 'test@example.com',
|
||||
subject: '【Whale Town】邮箱验证码',
|
||||
text: '您的验证码是:123456,5分钟内有效,请勿泄露给他人。'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('应该成功发送密码重置验证码', async () => {
|
||||
const options: VerificationEmailOptions = {
|
||||
email: 'test@example.com',
|
||||
code: '654321',
|
||||
nickname: '测试用户',
|
||||
purpose: 'password_reset'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
configService.get.mockReturnValue('"Test Sender" <noreply@test.com>');
|
||||
|
||||
const result = await service.sendVerificationCode(options);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: 'test@example.com',
|
||||
subject: '【Whale Town】密码重置验证码',
|
||||
text: '您的验证码是:654321,5分钟内有效,请勿泄露给他人。'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('应该在发送失败时返回false', async () => {
|
||||
const options: VerificationEmailOptions = {
|
||||
email: 'test@example.com',
|
||||
code: '123456',
|
||||
purpose: 'email_verification'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('发送失败'));
|
||||
|
||||
const result = await service.sendVerificationCode(options);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendWelcomeEmail', () => {
|
||||
it('应该成功发送欢迎邮件', async () => {
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
configService.get.mockReturnValue('"Test Sender" <noreply@test.com>');
|
||||
|
||||
const result = await service.sendWelcomeEmail('test@example.com', '测试用户');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: 'test@example.com',
|
||||
subject: '🎮 欢迎加入 Whale Town!',
|
||||
text: '欢迎 测试用户 加入 Whale Town 像素游戏世界!'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('应该在发送失败时返回false', async () => {
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('发送失败'));
|
||||
|
||||
const result = await service.sendWelcomeEmail('test@example.com', '测试用户');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyConnection', () => {
|
||||
it('应该在连接成功时返回true', async () => {
|
||||
mockTransporter.verify.mockResolvedValue(true);
|
||||
|
||||
const result = await service.verifyConnection();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.verify).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在连接失败时返回false', async () => {
|
||||
mockTransporter.verify.mockRejectedValue(new Error('连接失败'));
|
||||
|
||||
const result = await service.verifyConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('邮件模板测试', () => {
|
||||
it('应该生成包含验证码的邮箱验证模板', async () => {
|
||||
const options: VerificationEmailOptions = {
|
||||
email: 'test@example.com',
|
||||
code: '123456',
|
||||
nickname: '测试用户',
|
||||
purpose: 'email_verification'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockImplementation((mailOptions: any) => {
|
||||
expect(mailOptions.html).toContain('123456');
|
||||
expect(mailOptions.html).toContain('测试用户');
|
||||
expect(mailOptions.html).toContain('邮箱验证');
|
||||
expect(mailOptions.html).toContain('Whale Town');
|
||||
return Promise.resolve({ messageId: 'test-id' });
|
||||
});
|
||||
|
||||
await service.sendVerificationCode(options);
|
||||
});
|
||||
|
||||
it('应该生成包含验证码的密码重置模板', async () => {
|
||||
const options: VerificationEmailOptions = {
|
||||
email: 'test@example.com',
|
||||
code: '654321',
|
||||
nickname: '测试用户',
|
||||
purpose: 'password_reset'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockImplementation((mailOptions: any) => {
|
||||
expect(mailOptions.html).toContain('654321');
|
||||
expect(mailOptions.html).toContain('测试用户');
|
||||
expect(mailOptions.html).toContain('密码重置');
|
||||
expect(mailOptions.html).toContain('🔐');
|
||||
return Promise.resolve({ messageId: 'test-id' });
|
||||
});
|
||||
|
||||
await service.sendVerificationCode(options);
|
||||
});
|
||||
|
||||
it('应该生成包含用户昵称的欢迎邮件模板', async () => {
|
||||
mockTransporter.sendMail.mockImplementation((mailOptions: any) => {
|
||||
expect(mailOptions.html).toContain('测试用户');
|
||||
expect(mailOptions.html).toContain('欢迎加入 Whale Town');
|
||||
expect(mailOptions.html).toContain('🎮');
|
||||
expect(mailOptions.html).toContain('建造与创造');
|
||||
expect(mailOptions.html).toContain('社交互动');
|
||||
expect(mailOptions.html).toContain('任务挑战');
|
||||
return Promise.resolve({ messageId: 'test-id' });
|
||||
});
|
||||
|
||||
await service.sendWelcomeEmail('test@example.com', '测试用户');
|
||||
});
|
||||
|
||||
it('应该在没有昵称时正确处理模板', async () => {
|
||||
const options: VerificationEmailOptions = {
|
||||
email: 'test@example.com',
|
||||
code: '123456',
|
||||
purpose: 'email_verification'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockImplementation((mailOptions: any) => {
|
||||
expect(mailOptions.html).toContain('你好!');
|
||||
expect(mailOptions.html).not.toContain('你好 undefined');
|
||||
return Promise.resolve({ messageId: 'test-id' });
|
||||
});
|
||||
|
||||
await service.sendVerificationCode(options);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理测试', () => {
|
||||
it('应该正确处理网络错误', async () => {
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: '测试邮件',
|
||||
html: '<p>测试内容</p>'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('ECONNREFUSED'));
|
||||
|
||||
const result = await service.sendEmail(emailOptions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确处理认证错误', async () => {
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: '测试邮件',
|
||||
html: '<p>测试内容</p>'
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('Invalid login'));
|
||||
|
||||
const result = await service.sendEmail(emailOptions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确处理连接验证错误', async () => {
|
||||
mockTransporter.verify.mockRejectedValue(new Error('Connection timeout'));
|
||||
|
||||
const result = await service.verifyConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('配置测试', () => {
|
||||
it('应该使用默认配置值', () => {
|
||||
configService.get
|
||||
.mockReturnValueOnce(undefined) // EMAIL_HOST
|
||||
.mockReturnValueOnce(undefined) // EMAIL_PORT
|
||||
.mockReturnValueOnce(undefined) // EMAIL_SECURE
|
||||
.mockReturnValueOnce(undefined) // EMAIL_USER
|
||||
.mockReturnValueOnce(undefined); // EMAIL_PASS
|
||||
|
||||
const testService = new EmailService(configService);
|
||||
|
||||
expect(configService.get).toHaveBeenCalledWith('EMAIL_HOST', 'smtp.gmail.com');
|
||||
expect(configService.get).toHaveBeenCalledWith('EMAIL_PORT', 587);
|
||||
expect(configService.get).toHaveBeenCalledWith('EMAIL_SECURE', false);
|
||||
});
|
||||
|
||||
it('应该使用自定义配置值', () => {
|
||||
configService.get
|
||||
.mockReturnValueOnce('smtp.163.com') // EMAIL_HOST
|
||||
.mockReturnValueOnce(25) // EMAIL_PORT
|
||||
.mockReturnValueOnce(true) // EMAIL_SECURE
|
||||
.mockReturnValueOnce('custom@163.com') // EMAIL_USER
|
||||
.mockReturnValueOnce('custompass'); // EMAIL_PASS
|
||||
|
||||
const testService = new EmailService(configService);
|
||||
|
||||
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
||||
host: 'smtp.163.com',
|
||||
port: 25,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: 'custom@163.com',
|
||||
pass: 'custompass',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
370
src/core/utils/email/email.service.ts
Normal file
370
src/core/utils/email/email.service.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* 邮件服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供邮件发送的核心功能
|
||||
* - 支持多种邮件模板和场景
|
||||
* - 集成主流邮件服务提供商
|
||||
*
|
||||
* 支持的邮件类型:
|
||||
* - 邮箱验证码
|
||||
* - 密码重置验证码
|
||||
* - 欢迎邮件
|
||||
* - 系统通知
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user