resolve: 解决ANGJustinl-main与main分支的合并冲突

- 修复文件路径冲突(business/login -> business/auth结构调整)
- 保留ANGJustinl分支的验证码登录功能
- 合并main分支的用户状态管理和项目结构改进
- 修复邮件服务中缺失的login_verification模板问题
- 更新测试用例以包含验证码登录功能
- 统一导入路径以适配新的目录结构
This commit is contained in:
moyin
2025-12-25 15:11:14 +08:00
94 changed files with 8364 additions and 2029 deletions

View File

@@ -47,7 +47,7 @@ export interface VerificationEmailOptions {
/** 用户昵称 */
nickname?: string;
/** 验证码用途 */
purpose: 'email_verification' | 'password_reset' | 'login_verification';
purpose: 'email_verification' | 'password_reset';
}
/**
@@ -167,15 +167,9 @@ export class EmailService {
if (purpose === 'email_verification') {
subject = '【Whale Town】邮箱验证码';
template = this.getEmailVerificationTemplate(code, nickname);
} else if (purpose === 'password_reset') {
} else {
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({

View File

@@ -61,6 +61,15 @@ export class LogManagementService {
this.maxSize = this.configService.get('LOG_MAX_SIZE', '10m');
}
/**
* 获取日志目录的绝对路径
*
* 说明:用于后台打包下载 logs/ 整目录。
*/
getLogDirAbsolutePath(): string {
return path.resolve(this.logDir);
}
/**
* 定期清理过期日志文件
*
@@ -307,6 +316,67 @@ export class LogManagementService {
}
}
/**
* 获取运行日志尾部(用于后台查看)
*
* 说明:
* - 开发环境默认读取 dev.log
* - 生产环境默认读取 app.log可选 access/error
* - 通过读取文件尾部一定字节数实现“近似 tail”避免大文件全量读取
*/
async getRuntimeLogTail(options?: {
type?: 'app' | 'access' | 'error' | 'dev';
lines?: number;
}): Promise<{
file: string;
updated_at: string;
lines: string[];
}> {
const isProduction = this.configService.get('NODE_ENV') === 'production';
const requestedLines = Math.max(1, Math.min(Number(options?.lines ?? 200), 2000));
const requestedType = options?.type;
const allowedFiles = isProduction
? {
app: 'app.log',
access: 'access.log',
error: 'error.log',
}
: {
dev: 'dev.log',
};
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);
if (!fs.existsSync(filePath)) {
return { file: fileName, updated_at: new Date().toISOString(), lines: [] };
}
const stats = fs.statSync(filePath);
const maxBytes = 256 * 1024; // 256KB 足够覆盖常见的数百行日志
const readBytes = Math.min(stats.size, maxBytes);
const startPos = Math.max(0, stats.size - readBytes);
const fd = fs.openSync(filePath, 'r');
try {
const buffer = Buffer.alloc(readBytes);
fs.readSync(fd, buffer, 0, readBytes, startPos);
const text = buffer.toString('utf8');
const allLines = text.split(/\r?\n/).filter((l) => l.length > 0);
const tailLines = allLines.slice(-requestedLines);
return {
file: fileName,
updated_at: stats.mtime.toISOString(),
lines: tailLines,
};
} finally {
fs.closeSync(fd);
}
}
/**
* 解析最大文件数配置
*