feat: WWJCloud 企业级全栈框架 v0.3.5 完整更新
🚀 核心更新: - ✅ 完善 NestJS 企业级架构设计 - ✅ 优化配置中心和基础设施层 - ✅ 增强第三方服务集成能力 - ✅ 完善多租户架构支持 - 🎯 对标 Java Spring Boot 和 PHP ThinkPHP 📦 新增文件: - wwjcloud-nest 完整框架结构 - Docker 容器化配置 - 管理后台界面 - 数据库迁移脚本 🔑 Key: ebb38b43ec39f355f071294fd1cf9c42
This commit is contained in:
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
12
src/app.controller.ts
Normal file
12
src/app.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
33
src/app.module.ts
Normal file
33
src/app.module.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ConfigModule } from '@wwjConfig/config.module';
|
||||
import { CommonModule } from '@wwjCommon/common.module';
|
||||
import { VendorModule } from '@wwjVendor/vendor.module';
|
||||
import { CoreModule } from '@wwjCore/core.module';
|
||||
|
||||
/**
|
||||
* 应用根模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/module-ref
|
||||
*
|
||||
* 模块结构:
|
||||
* - ConfigModule: 配置中心(静态+动态配置)
|
||||
* - CommonModule: 基础设施层
|
||||
* - VendorModule: 第三方服务集成层
|
||||
* - CoreModule: 核心业务模块层
|
||||
*/
|
||||
@Module({
|
||||
imports: [ConfigModule, CommonModule, VendorModule, CoreModule],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
// 这里可以配置全局中间件
|
||||
// 例如:日志中间件、CORS 中间件等
|
||||
// consumer
|
||||
// .apply(LoggerMiddleware)
|
||||
// .forRoutes('*');
|
||||
}
|
||||
}
|
||||
8
src/app.service.ts
Normal file
8
src/app.service.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
64
src/common/base/base.entity.ts
Normal file
64
src/common/base/base.entity.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
VersionColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* 基础实体
|
||||
* 包含公共字段:ID、创建时间、更新时间、删除时间、是否删除、站点ID
|
||||
* 对应 Java: BaseEntity
|
||||
*/
|
||||
export abstract class BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@CreateDateColumn({
|
||||
name: 'create_time',
|
||||
type: 'timestamp',
|
||||
comment: '创建时间',
|
||||
})
|
||||
createTime: Date;
|
||||
|
||||
@UpdateDateColumn({
|
||||
name: 'update_time',
|
||||
type: 'timestamp',
|
||||
comment: '更新时间',
|
||||
})
|
||||
updateTime: Date;
|
||||
|
||||
@DeleteDateColumn({
|
||||
name: 'delete_time',
|
||||
type: 'timestamp',
|
||||
nullable: true,
|
||||
comment: '删除时间',
|
||||
})
|
||||
deleteTime: Date | null;
|
||||
|
||||
@Column({
|
||||
name: 'is_delete',
|
||||
type: 'tinyint',
|
||||
default: 0,
|
||||
comment: '是否删除 0:否 1:是',
|
||||
})
|
||||
isDelete: number;
|
||||
|
||||
@Column({
|
||||
name: 'site_id',
|
||||
type: 'int',
|
||||
default: 0,
|
||||
comment: '站点ID',
|
||||
})
|
||||
siteId: number;
|
||||
|
||||
@VersionColumn({
|
||||
name: 'version',
|
||||
type: 'int',
|
||||
default: 1,
|
||||
comment: '版本号',
|
||||
})
|
||||
version: number;
|
||||
}
|
||||
16
src/common/base/base.module.ts
Normal file
16
src/common/base/base.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 基础类模块 - 基础设施层
|
||||
* 提供基础实体、仓储、服务等抽象类
|
||||
* 对应 Java: BaseEntity, BaseMapper, BaseService
|
||||
*
|
||||
* 注意:此模块只提供抽象类,不提供具体实现
|
||||
* 抽象类不需要在 providers 中注册
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class BaseModule {}
|
||||
114
src/common/base/base.repository.ts
Normal file
114
src/common/base/base.repository.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
Repository,
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
DeepPartial,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
/**
|
||||
* 基础仓储
|
||||
* 提供通用的 CRUD、分页、软删除等操作
|
||||
* 对应 Java: BaseMapper
|
||||
*/
|
||||
export abstract class BaseRepository<T extends BaseEntity> {
|
||||
constructor(protected readonly repository: Repository<T>) {}
|
||||
|
||||
/**
|
||||
* 创建实体
|
||||
*/
|
||||
async create(entity: Partial<T>): Promise<T> {
|
||||
const newEntity = this.repository.create(entity as any);
|
||||
const result = await this.repository.save(newEntity);
|
||||
return Array.isArray(result) ? result[0] : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建
|
||||
*/
|
||||
async createMany(entities: Partial<T>[]): Promise<T[]> {
|
||||
const newEntities = this.repository.create(entities as any);
|
||||
return this.repository.save(newEntities);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找
|
||||
*/
|
||||
async findById(id: number): Promise<T | null> {
|
||||
return this.repository.findOne({ where: { id } } as FindOneOptions<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找单个实体
|
||||
*/
|
||||
async findOne(options: FindOneOptions<T>): Promise<T | null> {
|
||||
return this.repository.findOne(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找多个实体
|
||||
*/
|
||||
async find(options?: FindManyOptions<T>): Promise<T[]> {
|
||||
return this.repository.find(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
async paginate(
|
||||
page: number,
|
||||
limit: number,
|
||||
options?: FindManyOptions<T>,
|
||||
): Promise<{ data: T[]; total: number; page: number; limit: number }> {
|
||||
const [data, total] = await this.repository.findAndCount({
|
||||
...options,
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新实体
|
||||
*/
|
||||
async update(id: number, entity: Partial<T>): Promise<T | null> {
|
||||
await this.repository.update(id, entity as any);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
*/
|
||||
async softDelete(id: number): Promise<boolean> {
|
||||
const result = await this.repository.softDelete(id);
|
||||
return (result.affected || 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬删除
|
||||
*/
|
||||
async hardDelete(id: number): Promise<boolean> {
|
||||
const result = await this.repository.delete(id);
|
||||
return (result.affected || 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量软删除
|
||||
*/
|
||||
async softDeleteMany(ids: number[]): Promise<boolean> {
|
||||
const result = await this.repository.softDelete(ids);
|
||||
return (result.affected || 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计数量
|
||||
*/
|
||||
async count(options?: FindManyOptions<T>): Promise<number> {
|
||||
return this.repository.count(options);
|
||||
}
|
||||
}
|
||||
178
src/common/base/base.service.ts
Normal file
178
src/common/base/base.service.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Repository, FindManyOptions, FindOneOptions } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { PageResult } from '../response/page-result.class';
|
||||
|
||||
/**
|
||||
* 基础服务
|
||||
* 提供通用的业务逻辑抽象
|
||||
* 基于PHP和Java的BaseService统一设计
|
||||
*
|
||||
* 特点:
|
||||
* 1. 使用TypeORM原生Repository
|
||||
* 2. 统一分页格式 (与Java PageResult一致)
|
||||
* 3. 不处理响应格式 (由Controller层处理)
|
||||
* 4. 专注于业务逻辑
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class BaseService<T extends BaseEntity> {
|
||||
constructor(protected readonly repository: Repository<T>) {}
|
||||
|
||||
/**
|
||||
* 创建实体
|
||||
* @param entity 实体数据
|
||||
* @returns Promise<T>
|
||||
*/
|
||||
async create(entity: Partial<T>): Promise<T> {
|
||||
const newEntity = this.repository.create(entity as any);
|
||||
const result = await this.repository.save(newEntity);
|
||||
return Array.isArray(result) ? result[0] : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建
|
||||
* @param entities 实体数组
|
||||
* @returns Promise<T[]>
|
||||
*/
|
||||
async createMany(entities: Partial<T>[]): Promise<T[]> {
|
||||
const newEntities = this.repository.create(entities as any);
|
||||
return this.repository.save(newEntities);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找
|
||||
* @param id 主键ID
|
||||
* @returns Promise<T | null>
|
||||
*/
|
||||
async findById(id: number): Promise<T | null> {
|
||||
return this.repository.findOne({ where: { id } } as FindOneOptions<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找单个实体
|
||||
* @param options 查询选项
|
||||
* @returns Promise<T | null>
|
||||
*/
|
||||
async findOne(options: FindOneOptions<T>): Promise<T | null> {
|
||||
return this.repository.findOne(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找多个实体
|
||||
* @param options 查询选项
|
||||
* @returns Promise<T[]>
|
||||
*/
|
||||
async find(options?: FindManyOptions<T>): Promise<T[]> {
|
||||
return this.repository.find(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
* 返回格式与Java PageResult一致
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @param options 查询选项
|
||||
* @returns Promise<PageResult<T>>
|
||||
*/
|
||||
async paginate(
|
||||
page: number = 1,
|
||||
limit: number = 15,
|
||||
options?: FindManyOptions<T>,
|
||||
): Promise<PageResult<T>> {
|
||||
const [data, total] = await this.repository.findAndCount({
|
||||
...options,
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return new PageResult<T>(page, limit, total, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新实体
|
||||
* @param id 主键ID
|
||||
* @param entity 更新数据
|
||||
* @returns Promise<T | null>
|
||||
*/
|
||||
async update(id: number, entity: Partial<T>): Promise<T | null> {
|
||||
await this.repository.update(id, entity as any);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
* @param id 主键ID
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
async delete(id: number): Promise<boolean> {
|
||||
const result = await this.repository.softDelete(id);
|
||||
return (result.affected || 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬删除
|
||||
* @param id 主键ID
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
async hardDelete(id: number): Promise<boolean> {
|
||||
const result = await this.repository.delete(id);
|
||||
return (result.affected || 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量软删除
|
||||
* @param ids 主键ID数组
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
async deleteMany(ids: number[]): Promise<boolean> {
|
||||
const result = await this.repository.softDelete(ids);
|
||||
return (result.affected || 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计数量
|
||||
* @param options 查询选项
|
||||
* @returns Promise<number>
|
||||
*/
|
||||
async count(options?: FindManyOptions<T>): Promise<number> {
|
||||
return this.repository.count(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找单个实体
|
||||
* @param where 查询条件
|
||||
* @returns Promise<T | null>
|
||||
*/
|
||||
async findOneBy(where: Partial<T>): Promise<T | null> {
|
||||
return this.repository.findOne({ where } as FindOneOptions<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找多个实体
|
||||
* @param where 查询条件
|
||||
* @returns Promise<T[]>
|
||||
*/
|
||||
async findManyBy(where: Partial<T>): Promise<T[]> {
|
||||
return this.repository.find({ where } as FindManyOptions<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体是否存在
|
||||
* @param id 主键ID
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
async exists(id: number): Promise<boolean> {
|
||||
const count = await this.repository.count({ where: { id } } as FindManyOptions<T>);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件检查实体是否存在
|
||||
* @param where 查询条件
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
async existsBy(where: Partial<T>): Promise<boolean> {
|
||||
const count = await this.repository.count({ where } as FindManyOptions<T>);
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
172
src/common/cache/cache.interface.ts
vendored
Normal file
172
src/common/cache/cache.interface.ts
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 缓存接口定义
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: Cached 接口
|
||||
*/
|
||||
export interface CacheInterface {
|
||||
/**
|
||||
* 获取缓存
|
||||
* @param key 缓存键
|
||||
* @returns 缓存值
|
||||
*/
|
||||
get<T = any>(key: string): Promise<T | null>;
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
* @param key 缓存键
|
||||
* @param value 缓存值
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
set(key: string, value: any, ttl?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
* @param key 缓存键
|
||||
*/
|
||||
del(key: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 批量删除缓存
|
||||
* @param keys 缓存键数组
|
||||
*/
|
||||
delMany(keys: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在
|
||||
* @param key 缓存键
|
||||
* @returns 是否存在
|
||||
*/
|
||||
exists(key: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
* @param key 缓存键
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
expire(key: string, ttl: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取过期时间
|
||||
* @param key 缓存键
|
||||
* @returns 过期时间(秒)
|
||||
*/
|
||||
ttl(key: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 获取所有键
|
||||
* @param pattern 匹配模式
|
||||
* @returns 键数组
|
||||
*/
|
||||
keys(pattern?: string): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
flush(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
* @returns 统计信息
|
||||
*/
|
||||
stats(): Promise<CacheStats>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存统计信息
|
||||
*/
|
||||
export interface CacheStats {
|
||||
hits: number;
|
||||
misses: number;
|
||||
keys: number;
|
||||
memory: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存配置
|
||||
*/
|
||||
export interface CacheConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
password?: string;
|
||||
db?: number;
|
||||
keyPrefix?: string;
|
||||
defaultTtl?: number;
|
||||
maxRetries?: number;
|
||||
retryDelayOnFailover?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存装饰器选项
|
||||
*/
|
||||
export interface CacheOptions {
|
||||
/**
|
||||
* 缓存键
|
||||
*/
|
||||
key?: string;
|
||||
|
||||
/**
|
||||
* 过期时间(秒)
|
||||
*/
|
||||
ttl?: number;
|
||||
|
||||
/**
|
||||
* 是否启用缓存
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* 缓存条件
|
||||
*/
|
||||
condition?: (args: any[], result: any) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存辅助函数
|
||||
*/
|
||||
export interface CacheHelper<T> {
|
||||
execute(key: string): Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组缓存接口
|
||||
*/
|
||||
export interface GroupCacheInterface {
|
||||
/**
|
||||
* 设置分组缓存
|
||||
* @param group 分组名
|
||||
* @param key 缓存键
|
||||
* @param value 缓存值
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
set(group: string, key: string, value: any, ttl?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取分组缓存
|
||||
* @param group 分组名
|
||||
* @param key 缓存键
|
||||
* @returns 缓存值
|
||||
*/
|
||||
get<T = any>(group: string, key: string): Promise<T | null>;
|
||||
|
||||
/**
|
||||
* 删除分组缓存
|
||||
* @param group 分组名
|
||||
* @param key 缓存键
|
||||
*/
|
||||
del(group: string, key: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 清空分组缓存
|
||||
* @param group 分组名
|
||||
*/
|
||||
clear(group: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取分组所有键
|
||||
* @param group 分组名
|
||||
* @returns 键数组
|
||||
*/
|
||||
keys(group: string): Promise<string[]>;
|
||||
}
|
||||
170
src/common/cache/cache.module.ts
vendored
Normal file
170
src/common/cache/cache.module.ts
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { CacheService } from './cache.service';
|
||||
import { CacheInterface, GroupCacheInterface } from './cache.interface';
|
||||
|
||||
/**
|
||||
* 缓存模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: CacheConfig
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: 'CACHE_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 这里会根据配置选择具体的缓存实现
|
||||
// 默认使用内存缓存,生产环境使用 Redis
|
||||
const cacheType = configService.get('cache.type', 'memory');
|
||||
|
||||
if (cacheType === 'redis') {
|
||||
// 返回 Redis 缓存实现
|
||||
return {
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
// Redis 实现占位符
|
||||
return null;
|
||||
},
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
// Redis 实现占位符
|
||||
},
|
||||
async del(key: string): Promise<void> {
|
||||
// Redis 实现占位符
|
||||
},
|
||||
async delMany(keys: string[]): Promise<void> {
|
||||
// Redis 实现占位符
|
||||
},
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return false;
|
||||
},
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
// Redis 实现占位符
|
||||
},
|
||||
async ttl(key: string): Promise<number> {
|
||||
return -1;
|
||||
},
|
||||
async keys(pattern?: string): Promise<string[]> {
|
||||
return [];
|
||||
},
|
||||
async flush(): Promise<void> {
|
||||
// Redis 实现占位符
|
||||
},
|
||||
async stats(): Promise<any> {
|
||||
return { hits: 0, misses: 0, keys: 0, memory: 0, uptime: 0 };
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// 返回内存缓存实现
|
||||
const memoryCache = new Map();
|
||||
return {
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
const item = memoryCache.get(key);
|
||||
if (!item) return null;
|
||||
if (item.expire && Date.now() > item.expire) {
|
||||
memoryCache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return item.value;
|
||||
},
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
const expire = ttl ? Date.now() + ttl * 1000 : null;
|
||||
memoryCache.set(key, { value, expire });
|
||||
},
|
||||
async del(key: string): Promise<void> {
|
||||
memoryCache.delete(key);
|
||||
},
|
||||
async delMany(keys: string[]): Promise<void> {
|
||||
keys.forEach((key) => memoryCache.delete(key));
|
||||
},
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return memoryCache.has(key);
|
||||
},
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
const item = memoryCache.get(key);
|
||||
if (item) {
|
||||
item.expire = Date.now() + ttl * 1000;
|
||||
memoryCache.set(key, item);
|
||||
}
|
||||
},
|
||||
async ttl(key: string): Promise<number> {
|
||||
const item = memoryCache.get(key);
|
||||
if (!item || !item.expire) return -1;
|
||||
return Math.max(0, Math.floor((item.expire - Date.now()) / 1000));
|
||||
},
|
||||
async keys(pattern?: string): Promise<string[]> {
|
||||
return Array.from(memoryCache.keys());
|
||||
},
|
||||
async flush(): Promise<void> {
|
||||
memoryCache.clear();
|
||||
},
|
||||
async stats(): Promise<any> {
|
||||
return {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
keys: memoryCache.size,
|
||||
memory: 0,
|
||||
uptime: 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: 'GROUP_CACHE_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 分组缓存实现
|
||||
const groupCache = new Map();
|
||||
return {
|
||||
async set(
|
||||
group: string,
|
||||
key: string,
|
||||
value: any,
|
||||
ttl?: number,
|
||||
): Promise<void> {
|
||||
const groupKey = `${group}:${key}`;
|
||||
const expire = ttl ? Date.now() + ttl * 1000 : null;
|
||||
groupCache.set(groupKey, { value, expire });
|
||||
},
|
||||
async get<T = any>(group: string, key: string): Promise<T | null> {
|
||||
const groupKey = `${group}:${key}`;
|
||||
const item = groupCache.get(groupKey);
|
||||
if (!item) return null;
|
||||
if (item.expire && Date.now() > item.expire) {
|
||||
groupCache.delete(groupKey);
|
||||
return null;
|
||||
}
|
||||
return item.value;
|
||||
},
|
||||
async del(group: string, key: string): Promise<void> {
|
||||
const groupKey = `${group}:${key}`;
|
||||
groupCache.delete(groupKey);
|
||||
},
|
||||
async clear(group: string): Promise<void> {
|
||||
for (const [key] of groupCache) {
|
||||
if (key.startsWith(`${group}:`)) {
|
||||
groupCache.delete(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
async keys(group: string): Promise<string[]> {
|
||||
const keys: string[] = [];
|
||||
for (const [key] of groupCache) {
|
||||
if (key.startsWith(`${group}:`)) {
|
||||
keys.push(key.replace(`${group}:`, ''));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
CacheService,
|
||||
],
|
||||
exports: [CacheService],
|
||||
})
|
||||
export class CacheModule {}
|
||||
328
src/common/cache/cache.service.ts
vendored
Normal file
328
src/common/cache/cache.service.ts
vendored
Normal file
@@ -0,0 +1,328 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import type {
|
||||
CacheInterface,
|
||||
CacheOptions,
|
||||
CacheHelper,
|
||||
GroupCacheInterface,
|
||||
} from './cache.interface';
|
||||
|
||||
/**
|
||||
* 缓存服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: CachedService
|
||||
*/
|
||||
@Injectable()
|
||||
export class CacheService implements CacheInterface {
|
||||
private readonly logger = new Logger(CacheService.name);
|
||||
|
||||
constructor(
|
||||
@Inject('CACHE_PROVIDER') private readonly cacheProvider: CacheInterface,
|
||||
@Inject('GROUP_CACHE_PROVIDER')
|
||||
private readonly groupCacheProvider: GroupCacheInterface,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
try {
|
||||
return await this.cacheProvider.get<T>(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get cache key: ${key}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
try {
|
||||
await this.cacheProvider.set(key, value, ttl);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set cache key: ${key}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
async del(key: string): Promise<void> {
|
||||
try {
|
||||
await this.cacheProvider.del(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to delete cache key: ${key}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除缓存
|
||||
*/
|
||||
async delMany(keys: string[]): Promise<void> {
|
||||
try {
|
||||
await this.cacheProvider.delMany(keys);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to delete cache keys: ${keys.join(', ')}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在
|
||||
*/
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
return await this.cacheProvider.exists(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to check cache key: ${key}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
*/
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
try {
|
||||
await this.cacheProvider.expire(key, ttl);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set expire for cache key: ${key}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期时间
|
||||
*/
|
||||
async ttl(key: string): Promise<number> {
|
||||
try {
|
||||
return await this.cacheProvider.ttl(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get ttl for cache key: ${key}`, error);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有键
|
||||
*/
|
||||
async keys(pattern?: string): Promise<string[]> {
|
||||
try {
|
||||
return await this.cacheProvider.keys(pattern);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to get cache keys with pattern: ${pattern}`,
|
||||
error,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
async flush(): Promise<void> {
|
||||
try {
|
||||
await this.cacheProvider.flush();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to flush cache', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
async stats() {
|
||||
try {
|
||||
return await this.cacheProvider.stats();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get cache stats', error);
|
||||
return {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
keys: 0,
|
||||
memory: 0,
|
||||
uptime: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存装饰器实现
|
||||
* 对应 Java: cache(String key, CacheHelper<T> cacheHelper)
|
||||
*/
|
||||
async cache<T>(key: string, cacheHelper: CacheHelper<T>): Promise<T> {
|
||||
return this.cacheWithOptions<T>({ key }, cacheHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带选项的缓存
|
||||
* 对应 Java: cache(boolean cache, String key, CacheHelper<T> cacheHelper)
|
||||
*/
|
||||
async cacheWithOptions<T>(
|
||||
options: CacheOptions,
|
||||
cacheHelper: CacheHelper<T>,
|
||||
): Promise<T> {
|
||||
const { key, ttl, enabled = true, condition } = options;
|
||||
|
||||
if (!enabled || !key) {
|
||||
return await cacheHelper.execute(key || '');
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试从缓存获取
|
||||
let result = await this.get<T>(key);
|
||||
|
||||
if (result === null) {
|
||||
// 缓存未命中,执行函数
|
||||
result = await cacheHelper.execute(key);
|
||||
|
||||
if (result !== null && result !== undefined) {
|
||||
// 检查缓存条件
|
||||
if (!condition || condition([], result)) {
|
||||
await this.set(key, result, ttl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache operation failed for key: ${key}`, error);
|
||||
// 降级到直接执行
|
||||
return await cacheHelper.execute(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记住结果(带参数)
|
||||
* 对应 Java: remember(List<Object> paramList, CacheHelper<T> cacheHelper)
|
||||
*/
|
||||
async remember<T>(paramList: any[], cacheHelper: CacheHelper<T>): Promise<T> {
|
||||
const key = this.computeUniqueKey(paramList);
|
||||
return this.cache<T>(key, cacheHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记住对象结果
|
||||
* 对应 Java: rememberObject(List<Object> paramList, CacheHelper<T> cacheHelper)
|
||||
*/
|
||||
async rememberObject<T>(
|
||||
paramList: any[],
|
||||
cacheHelper: CacheHelper<T>,
|
||||
): Promise<T> {
|
||||
return this.remember<T>(paramList, cacheHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组缓存
|
||||
* 对应 Java: tag(String group)
|
||||
*/
|
||||
tag(group: string) {
|
||||
return {
|
||||
set: (key: string, value: any, ttl?: number) =>
|
||||
this.groupCacheProvider.set(group, key, value, ttl),
|
||||
get: <T = any>(key: string) => this.groupCacheProvider.get<T>(group, key),
|
||||
del: (key: string) => this.groupCacheProvider.del(group, key),
|
||||
clear: () => this.groupCacheProvider.clear(group),
|
||||
keys: () => this.groupCacheProvider.keys(group),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算唯一键
|
||||
* 对应 Java: CacheUtils.computeUniqueKey(paramList)
|
||||
*/
|
||||
private computeUniqueKey(paramList: any[]): string {
|
||||
if (!paramList || paramList.length === 0) {
|
||||
return 'empty';
|
||||
}
|
||||
|
||||
const keyParts = paramList.map((param) => {
|
||||
if (param === null || param === undefined) {
|
||||
return 'null';
|
||||
}
|
||||
if (typeof param === 'object') {
|
||||
return JSON.stringify(param);
|
||||
}
|
||||
return String(param);
|
||||
});
|
||||
|
||||
return keyParts.join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作
|
||||
*/
|
||||
async mget<T = any>(keys: string[]): Promise<(T | null)[]> {
|
||||
const results: (T | null)[] = [];
|
||||
for (const key of keys) {
|
||||
results.push(await this.get<T>(key));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置
|
||||
*/
|
||||
async mset(
|
||||
keyValuePairs: Array<{ key: string; value: any; ttl?: number }>,
|
||||
): Promise<void> {
|
||||
for (const { key, value, ttl } of keyValuePairs) {
|
||||
await this.set(key, value, ttl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
async incr(key: string, increment: number = 1): Promise<number> {
|
||||
const current = (await this.get<number>(key)) || 0;
|
||||
const newValue = current + increment;
|
||||
await this.set(key, newValue);
|
||||
return newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减
|
||||
*/
|
||||
async decr(key: string, decrement: number = 1): Promise<number> {
|
||||
return this.incr(key, -decrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置哈希字段
|
||||
*/
|
||||
async hset(key: string, field: string, value: any): Promise<void> {
|
||||
const hash = (await this.get<Record<string, any>>(key)) || {};
|
||||
hash[field] = value;
|
||||
await this.set(key, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希字段
|
||||
*/
|
||||
async hget<T = any>(key: string, field: string): Promise<T | null> {
|
||||
const hash = await this.get<Record<string, any>>(key);
|
||||
return hash ? hash[field] || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除哈希字段
|
||||
*/
|
||||
async hdel(key: string, field: string): Promise<void> {
|
||||
const hash = await this.get<Record<string, any>>(key);
|
||||
if (hash) {
|
||||
delete hash[field];
|
||||
await this.set(key, hash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希所有字段
|
||||
*/
|
||||
async hgetall(key: string): Promise<Record<string, any>> {
|
||||
return (await this.get<Record<string, any>>(key)) || {};
|
||||
}
|
||||
}
|
||||
120
src/common/cache/decorators/cache-evict.decorator.ts
vendored
Normal file
120
src/common/cache/decorators/cache-evict.decorator.ts
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 缓存清除键生成器
|
||||
*/
|
||||
export type CacheEvictKeyGenerator = (
|
||||
target: any,
|
||||
methodName: string,
|
||||
args: any[],
|
||||
) => string | string[];
|
||||
|
||||
/**
|
||||
* 缓存清除装饰器选项
|
||||
*/
|
||||
export interface CacheEvictOptions {
|
||||
/**
|
||||
* 要清除的缓存键
|
||||
*/
|
||||
key?: string | string[] | CacheEvictKeyGenerator;
|
||||
|
||||
/**
|
||||
* 是否在方法执行前清除
|
||||
*/
|
||||
beforeInvocation?: boolean;
|
||||
|
||||
/**
|
||||
* 是否清除所有缓存
|
||||
*/
|
||||
allEntries?: boolean;
|
||||
|
||||
/**
|
||||
* 缓存分组
|
||||
*/
|
||||
group?: string;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存清除装饰器元数据键
|
||||
*/
|
||||
export const CACHE_EVICT_METADATA_KEY = 'cacheEvict';
|
||||
|
||||
/**
|
||||
* 缓存清除装饰器
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/custom-decorators
|
||||
* 对应 Java: @CacheEvict
|
||||
*/
|
||||
export const CacheEvict = (options: CacheEvictOptions = {}) => {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
SetMetadata(CACHE_EVICT_METADATA_KEY, {
|
||||
key: options.key || propertyKey,
|
||||
beforeInvocation: options.beforeInvocation || false,
|
||||
allEntries: options.allEntries || false,
|
||||
group: options.group,
|
||||
enabled: options.enabled !== false,
|
||||
})(target, propertyKey, descriptor);
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认缓存清除键生成器
|
||||
*/
|
||||
export const defaultCacheEvictKeyGenerator: CacheEvictKeyGenerator = (
|
||||
target,
|
||||
methodName,
|
||||
args,
|
||||
) => {
|
||||
const className = target.constructor.name;
|
||||
const argsStr = args
|
||||
.map((arg) => {
|
||||
if (arg === null || arg === undefined) {
|
||||
return 'null';
|
||||
}
|
||||
if (typeof arg === 'object') {
|
||||
return JSON.stringify(arg);
|
||||
}
|
||||
return String(arg);
|
||||
})
|
||||
.join(':');
|
||||
|
||||
return `${className}:${methodName}:${argsStr}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 简化版缓存清除装饰器
|
||||
* @param key 要清除的缓存键
|
||||
*/
|
||||
export const EvictCache = (key?: string) => {
|
||||
return CacheEvict({ key });
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除所有缓存装饰器
|
||||
*/
|
||||
export const EvictAllCache = () => {
|
||||
return CacheEvict({ allEntries: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* 分组缓存清除装饰器
|
||||
* @param group 分组名
|
||||
*/
|
||||
export const EvictGroupCache = (group: string) => {
|
||||
return CacheEvict({ group, allEntries: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* 方法执行前清除缓存装饰器
|
||||
* @param key 要清除的缓存键
|
||||
*/
|
||||
export const EvictCacheBefore = (key?: string) => {
|
||||
return CacheEvict({ key, beforeInvocation: true });
|
||||
};
|
||||
124
src/common/cache/decorators/cacheable.decorator.ts
vendored
Normal file
124
src/common/cache/decorators/cacheable.decorator.ts
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 缓存键生成器
|
||||
*/
|
||||
export type CacheKeyGenerator = (
|
||||
target: any,
|
||||
methodName: string,
|
||||
args: any[],
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* 缓存条件检查器
|
||||
*/
|
||||
export type CacheCondition = (args: any[], result: any) => boolean;
|
||||
|
||||
/**
|
||||
* 缓存装饰器选项
|
||||
*/
|
||||
export interface CacheableOptions {
|
||||
/**
|
||||
* 缓存键
|
||||
*/
|
||||
key?: string | CacheKeyGenerator;
|
||||
|
||||
/**
|
||||
* 过期时间(秒)
|
||||
*/
|
||||
ttl?: number;
|
||||
|
||||
/**
|
||||
* 是否启用缓存
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* 缓存条件
|
||||
*/
|
||||
condition?: CacheCondition;
|
||||
|
||||
/**
|
||||
* 缓存分组
|
||||
*/
|
||||
group?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存装饰器元数据键
|
||||
*/
|
||||
export const CACHEABLE_METADATA_KEY = 'cacheable';
|
||||
|
||||
/**
|
||||
* 缓存装饰器
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/custom-decorators
|
||||
* 对应 Java: @Cacheable
|
||||
*/
|
||||
export const Cacheable = (options: CacheableOptions = {}) => {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
SetMetadata(CACHEABLE_METADATA_KEY, {
|
||||
key: options.key || propertyKey,
|
||||
ttl: options.ttl || 300, // 默认5分钟
|
||||
enabled: options.enabled !== false,
|
||||
condition: options.condition,
|
||||
group: options.group,
|
||||
})(target, propertyKey, descriptor);
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认缓存键生成器
|
||||
*/
|
||||
export const defaultCacheKeyGenerator: CacheKeyGenerator = (
|
||||
target,
|
||||
methodName,
|
||||
args,
|
||||
) => {
|
||||
const className = target.constructor.name;
|
||||
const argsStr = args
|
||||
.map((arg) => {
|
||||
if (arg === null || arg === undefined) {
|
||||
return 'null';
|
||||
}
|
||||
if (typeof arg === 'object') {
|
||||
return JSON.stringify(arg);
|
||||
}
|
||||
return String(arg);
|
||||
})
|
||||
.join(':');
|
||||
|
||||
return `${className}:${methodName}:${argsStr}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 简化版缓存装饰器
|
||||
* @param ttl 过期时间(秒)
|
||||
* @param key 缓存键
|
||||
*/
|
||||
export const Cache = (ttl: number = 300, key?: string) => {
|
||||
return Cacheable({ ttl, key });
|
||||
};
|
||||
|
||||
/**
|
||||
* 分组缓存装饰器
|
||||
* @param group 分组名
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
export const GroupCache = (group: string, ttl: number = 300) => {
|
||||
return Cacheable({ group, ttl });
|
||||
};
|
||||
|
||||
/**
|
||||
* 条件缓存装饰器
|
||||
* @param condition 缓存条件
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
export const ConditionalCache = (
|
||||
condition: CacheCondition,
|
||||
ttl: number = 300,
|
||||
) => {
|
||||
return Cacheable({ condition, ttl });
|
||||
};
|
||||
98
src/common/common.module.ts
Normal file
98
src/common/common.module.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { CacheModule } from './cache/cache.module';
|
||||
import { MonitoringModule } from './monitoring/monitoring.module';
|
||||
import { LoggingModule } from './logging/logging.module';
|
||||
import { ExceptionModule } from './exception/exception.module';
|
||||
import { QueueModule } from './queue/queue.module';
|
||||
import { EventModule } from './event/event.module';
|
||||
import { SecurityModule } from './security/security.module';
|
||||
import { TracingModule } from './tracing/tracing.module';
|
||||
import { SchedulerModule } from './scheduler/scheduler.module';
|
||||
import { InitModule } from './init/init.module';
|
||||
import { ContextModule } from './context/context.module';
|
||||
// import { SwaggerModule } from './swagger/swagger.module';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
// import { ValidationModule } from './validation/validation.module'; // 已合并到 PipesModule
|
||||
import { ResponseModule } from './response/response.module';
|
||||
import { UtilsModule } from './utils/utils.module';
|
||||
import { PipesModule } from './pipes/pipes.module';
|
||||
import { SystemModule } from './system/system.module';
|
||||
import { LoaderModule } from './loader/loader.module';
|
||||
import { BaseModule } from './base/base.module';
|
||||
import { InterceptorsModule } from './interceptors/interceptors.module';
|
||||
import { LibrariesModule } from './libraries/libraries.module';
|
||||
import { PluginsModule } from './plugins/plugins.module';
|
||||
|
||||
/**
|
||||
* 通用模块 - 基础设施层
|
||||
* 基于 NestJS 官方示例实现
|
||||
*
|
||||
* 包含框架基础能力:
|
||||
* - 基础服务(缓存、日志、监控、异常)
|
||||
* - 自研工具类
|
||||
* - 第三方工具库封装
|
||||
* - 基础功能插件
|
||||
* - 框架基础设施
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
// 核心基础设施模块(需要全局访问)
|
||||
DatabaseModule, // 数据库连接
|
||||
CacheModule, // 缓存服务
|
||||
MonitoringModule, // 监控服务
|
||||
LoggingModule, // 日志服务
|
||||
ExceptionModule, // 异常处理
|
||||
QueueModule, // 队列服务
|
||||
EventModule, // 事件服务
|
||||
ResponseModule, // 响应处理
|
||||
|
||||
// 自研工具类
|
||||
UtilsModule,
|
||||
|
||||
// 第三方工具库封装
|
||||
LibrariesModule,
|
||||
|
||||
// 基础功能插件
|
||||
PluginsModule,
|
||||
|
||||
// 框架基础设施
|
||||
SecurityModule,
|
||||
TracingModule,
|
||||
SchedulerModule,
|
||||
InitModule,
|
||||
ContextModule,
|
||||
// SwaggerModule,
|
||||
SystemModule,
|
||||
LoaderModule,
|
||||
BaseModule,
|
||||
InterceptorsModule,
|
||||
PipesModule,
|
||||
],
|
||||
exports: [
|
||||
// 核心基础设施服务
|
||||
DatabaseModule,
|
||||
CacheModule,
|
||||
MonitoringModule,
|
||||
LoggingModule,
|
||||
ExceptionModule,
|
||||
QueueModule,
|
||||
EventModule,
|
||||
ResponseModule,
|
||||
|
||||
// 自研工具类
|
||||
UtilsModule,
|
||||
|
||||
// 第三方工具库封装
|
||||
LibrariesModule,
|
||||
|
||||
// 基础功能插件
|
||||
PluginsModule,
|
||||
|
||||
// 框架基础设施
|
||||
SecurityModule,
|
||||
BaseModule,
|
||||
InterceptorsModule,
|
||||
],
|
||||
})
|
||||
export class CommonModule {}
|
||||
13
src/common/context/context.module.ts
Normal file
13
src/common/context/context.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ContextService } from './context.service';
|
||||
|
||||
/**
|
||||
* 上下文模块 - 基础设施层
|
||||
* 提供请求上下文管理功能
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [ContextService],
|
||||
exports: [ContextService],
|
||||
})
|
||||
export class ContextModule {}
|
||||
86
src/common/context/context.service.ts
Normal file
86
src/common/context/context.service.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
|
||||
/**
|
||||
* 上下文服务
|
||||
* 提供请求上下文管理功能
|
||||
*/
|
||||
@Injectable()
|
||||
export class ContextService {
|
||||
private readonly contextMap = new Map<string, any>();
|
||||
|
||||
/**
|
||||
* 设置上下文数据
|
||||
*/
|
||||
setContext(key: string, value: any) {
|
||||
this.contextMap.set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上下文数据
|
||||
*/
|
||||
getContext<T = any>(key: string): T | undefined {
|
||||
return this.contextMap.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除上下文数据
|
||||
*/
|
||||
clearContext(key?: string) {
|
||||
if (key) {
|
||||
this.contextMap.delete(key);
|
||||
} else {
|
||||
this.contextMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取用户信息
|
||||
*/
|
||||
getUserFromRequest(request: Request) {
|
||||
return (request as any).user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取站点信息
|
||||
*/
|
||||
getSiteFromRequest(request: Request) {
|
||||
return (request as any).site;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户 ID
|
||||
*/
|
||||
getCurrentUserId(request: Request): number | undefined {
|
||||
const user = this.getUserFromRequest(request);
|
||||
return user?.userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前站点 ID
|
||||
*/
|
||||
getCurrentSiteId(request: Request): number | undefined {
|
||||
const site = this.getSiteFromRequest(request);
|
||||
return site?.siteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端 IP
|
||||
*/
|
||||
getClientIp(request: Request): string {
|
||||
return (
|
||||
(request.headers['x-forwarded-for'] as string) ||
|
||||
(request.headers['x-real-ip'] as string) ||
|
||||
request.connection.remoteAddress ||
|
||||
request.socket.remoteAddress ||
|
||||
'127.0.0.1'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户代理
|
||||
*/
|
||||
getUserAgent(request: Request): string {
|
||||
return request.headers['user-agent'] || '';
|
||||
}
|
||||
}
|
||||
468
src/common/database/backup.service.ts
Normal file
468
src/common/database/backup.service.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as archiver from 'archiver';
|
||||
import { SystemUtil } from '../utils/system.util';
|
||||
|
||||
/**
|
||||
* 数据库备份服务
|
||||
* 基于 TypeORM 实现
|
||||
* 对应 Java: DatabaseBackup
|
||||
*/
|
||||
@Injectable()
|
||||
export class DatabaseBackupService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly systemUtil: SystemUtil,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 备份数据库
|
||||
*/
|
||||
async backupDatabase(options?: {
|
||||
tables?: string[];
|
||||
excludeTables?: string[];
|
||||
outputPath?: string;
|
||||
compress?: boolean;
|
||||
includeData?: boolean;
|
||||
includeSchema?: boolean;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
filePath: string;
|
||||
size: number;
|
||||
tables: number;
|
||||
message: string;
|
||||
}> {
|
||||
const config = {
|
||||
tables: options?.tables || [],
|
||||
excludeTables: options?.excludeTables || [],
|
||||
outputPath: options?.outputPath || this.getDefaultBackupPath(),
|
||||
compress: options?.compress !== false,
|
||||
includeData: options?.includeData !== false,
|
||||
includeSchema: options?.includeSchema !== false,
|
||||
};
|
||||
|
||||
try {
|
||||
// 确保输出目录存在
|
||||
await this.systemUtil.createDirectory(path.dirname(config.outputPath));
|
||||
|
||||
// 获取所有表
|
||||
const allTables = await this.getAllTables();
|
||||
const tablesToBackup = this.filterTables(
|
||||
allTables,
|
||||
config.tables,
|
||||
config.excludeTables,
|
||||
);
|
||||
|
||||
if (tablesToBackup.length === 0) {
|
||||
throw new Error('没有找到要备份的表');
|
||||
}
|
||||
|
||||
// 生成备份文件名
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupFileName = `backup_${timestamp}.sql`;
|
||||
const backupFilePath = path.join(config.outputPath, backupFileName);
|
||||
|
||||
// 执行备份
|
||||
const backupContent = await this.generateBackupContent(
|
||||
tablesToBackup,
|
||||
config,
|
||||
);
|
||||
await this.systemUtil.writeFile(backupFilePath, backupContent);
|
||||
|
||||
// 压缩备份文件
|
||||
let finalFilePath = backupFilePath;
|
||||
if (config.compress) {
|
||||
finalFilePath = await this.compressBackupFile(backupFilePath);
|
||||
// 删除原始文件
|
||||
await this.systemUtil.removeFileOrDirectory(backupFilePath);
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
const fileInfo = await this.systemUtil.getFileInfo(finalFilePath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filePath: finalFilePath,
|
||||
size: fileInfo.size,
|
||||
tables: tablesToBackup.length,
|
||||
message: '数据库备份成功',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
filePath: '',
|
||||
size: 0,
|
||||
tables: 0,
|
||||
message: `数据库备份失败: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复数据库
|
||||
*/
|
||||
async restoreDatabase(
|
||||
backupFilePath: string,
|
||||
options?: {
|
||||
tables?: string[];
|
||||
excludeTables?: string[];
|
||||
dropTables?: boolean;
|
||||
},
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
}> {
|
||||
try {
|
||||
// 检查备份文件是否存在
|
||||
if (!(await this.systemUtil.fileExists(backupFilePath))) {
|
||||
throw new Error('备份文件不存在');
|
||||
}
|
||||
|
||||
// 解压备份文件(如果需要)
|
||||
let sqlFilePath = backupFilePath;
|
||||
if (backupFilePath.endsWith('.zip')) {
|
||||
sqlFilePath = await this.extractBackupFile(backupFilePath);
|
||||
}
|
||||
|
||||
// 读取 SQL 内容
|
||||
const sqlContent = await this.systemUtil.readFile(sqlFilePath);
|
||||
|
||||
// 解析 SQL 内容
|
||||
const sqlStatements = this.parseSqlContent(sqlContent);
|
||||
|
||||
// 执行恢复
|
||||
await this.executeRestore(sqlStatements, options);
|
||||
|
||||
// 清理临时文件
|
||||
if (sqlFilePath !== backupFilePath) {
|
||||
await this.systemUtil.removeFileOrDirectory(sqlFilePath);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '数据库恢复成功',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `数据库恢复失败: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有表
|
||||
*/
|
||||
private async getAllTables(): Promise<string[]> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
try {
|
||||
const tables = await queryRunner.query(`
|
||||
SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
ORDER BY TABLE_NAME
|
||||
`);
|
||||
return tables.map((table: any) => table.TABLE_NAME);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤表
|
||||
*/
|
||||
private filterTables(
|
||||
allTables: string[],
|
||||
includeTables: string[],
|
||||
excludeTables: string[],
|
||||
): string[] {
|
||||
let filteredTables = allTables;
|
||||
|
||||
// 包含指定表
|
||||
if (includeTables.length > 0) {
|
||||
filteredTables = filteredTables.filter((table) =>
|
||||
includeTables.includes(table),
|
||||
);
|
||||
}
|
||||
|
||||
// 排除指定表
|
||||
if (excludeTables.length > 0) {
|
||||
filteredTables = filteredTables.filter(
|
||||
(table) => !excludeTables.includes(table),
|
||||
);
|
||||
}
|
||||
|
||||
return filteredTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成备份内容
|
||||
*/
|
||||
private async generateBackupContent(
|
||||
tables: string[],
|
||||
config: any,
|
||||
): Promise<string> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
let backupContent = '';
|
||||
|
||||
try {
|
||||
// 添加备份头部信息
|
||||
backupContent += this.generateBackupHeader();
|
||||
|
||||
// 备份表结构
|
||||
if (config.includeSchema) {
|
||||
for (const table of tables) {
|
||||
backupContent += await this.generateTableSchema(queryRunner, table);
|
||||
}
|
||||
}
|
||||
|
||||
// 备份表数据
|
||||
if (config.includeData) {
|
||||
for (const table of tables) {
|
||||
backupContent += await this.generateTableData(queryRunner, table);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加备份尾部信息
|
||||
backupContent += this.generateBackupFooter();
|
||||
|
||||
return backupContent;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成备份头部
|
||||
*/
|
||||
private generateBackupHeader(): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
return `
|
||||
-- WWJCloud Database Backup
|
||||
-- Generated at: ${timestamp}
|
||||
-- Database: ${this.dataSource.options.database}
|
||||
-- Version: ${this.dataSource.options.type}
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
|
||||
SET AUTOCOMMIT=0;
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成备份尾部
|
||||
*/
|
||||
private generateBackupFooter(): string {
|
||||
return `
|
||||
COMMIT;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成表结构
|
||||
*/
|
||||
private async generateTableSchema(
|
||||
queryRunner: any,
|
||||
tableName: string,
|
||||
): Promise<string> {
|
||||
const createTableResult = await queryRunner.query(
|
||||
`SHOW CREATE TABLE \`${tableName}\``,
|
||||
);
|
||||
const createTableSql = createTableResult[0]['Create Table'];
|
||||
|
||||
return `
|
||||
-- Table structure for table \`${tableName}\`
|
||||
DROP TABLE IF EXISTS \`${tableName}\`;
|
||||
${createTableSql};
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成表数据
|
||||
*/
|
||||
private async generateTableData(
|
||||
queryRunner: any,
|
||||
tableName: string,
|
||||
): Promise<string> {
|
||||
const rows = await queryRunner.query(`SELECT * FROM \`${tableName}\``);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return `-- Data for table \`${tableName}\` (empty)\n\n`;
|
||||
}
|
||||
|
||||
// 获取列名
|
||||
const columns = Object.keys(rows[0]);
|
||||
const columnNames = columns.map((col) => `\`${col}\``).join(', ');
|
||||
|
||||
// 生成 INSERT 语句
|
||||
let insertStatements = `-- Data for table \`${tableName}\`\n`;
|
||||
|
||||
for (const row of rows) {
|
||||
const values = columns
|
||||
.map((col) => {
|
||||
const value = row[col];
|
||||
if (value === null) return 'NULL';
|
||||
if (typeof value === 'string')
|
||||
return `'${value.replace(/'/g, "''")}'`;
|
||||
if (value instanceof Date)
|
||||
return `'${value.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
||||
return value;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
insertStatements += `INSERT INTO \`${tableName}\` (${columnNames}) VALUES (${values});\n`;
|
||||
}
|
||||
|
||||
return insertStatements + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩备份文件
|
||||
*/
|
||||
private async compressBackupFile(filePath: string): Promise<string> {
|
||||
const zipPath = filePath.replace('.sql', '.zip');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = fs.createWriteStream(zipPath);
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
|
||||
output.on('close', () => {
|
||||
resolve(zipPath);
|
||||
});
|
||||
|
||||
archive.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
archive.file(filePath, { name: path.basename(filePath) });
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压备份文件
|
||||
*/
|
||||
private async extractBackupFile(zipPath: string): Promise<string> {
|
||||
// 这里需要实现解压逻辑
|
||||
// 由于 archiver 主要用于压缩,解压需要使用其他库如 yauzl
|
||||
throw new Error('解压功能需要额外实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 SQL 内容
|
||||
*/
|
||||
private parseSqlContent(sqlContent: string): string[] {
|
||||
// 简单的 SQL 语句分割
|
||||
return sqlContent
|
||||
.split(';')
|
||||
.map((stmt) => stmt.trim())
|
||||
.filter((stmt) => stmt.length > 0 && !stmt.startsWith('--'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行恢复
|
||||
*/
|
||||
private async executeRestore(
|
||||
sqlStatements: string[],
|
||||
options?: any,
|
||||
): Promise<void> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
for (const statement of sqlStatements) {
|
||||
if (statement.trim()) {
|
||||
await queryRunner.query(statement);
|
||||
}
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认备份路径
|
||||
*/
|
||||
private getDefaultBackupPath(): string {
|
||||
const backupDir = this.configService.get('database.backupPath', 'backups');
|
||||
return path.join(this.systemUtil.getAppRootDirectory(), backupDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份列表
|
||||
*/
|
||||
async getBackupList(): Promise<{
|
||||
files: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
createdAt: Date;
|
||||
modifiedAt: Date;
|
||||
}>;
|
||||
}> {
|
||||
const backupDir = this.getDefaultBackupPath();
|
||||
|
||||
if (!(await this.systemUtil.fileExists(backupDir))) {
|
||||
return { files: [] };
|
||||
}
|
||||
|
||||
const files = await this.systemUtil.readDirectory(backupDir);
|
||||
const backupFiles = files.filter(
|
||||
(file) => file.endsWith('.sql') || file.endsWith('.zip'),
|
||||
);
|
||||
|
||||
const fileInfos = await Promise.all(
|
||||
backupFiles.map(async (file) => {
|
||||
const filePath = path.join(backupDir, file);
|
||||
const fileInfo = await this.systemUtil.getFileInfo(filePath);
|
||||
|
||||
return {
|
||||
name: file,
|
||||
path: filePath,
|
||||
size: fileInfo.size,
|
||||
createdAt: fileInfo.ctime,
|
||||
modifiedAt: fileInfo.mtime,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return { files: fileInfos };
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除备份文件
|
||||
*/
|
||||
async deleteBackupFile(filePath: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
}> {
|
||||
try {
|
||||
await this.systemUtil.removeFileOrDirectory(filePath);
|
||||
return {
|
||||
success: true,
|
||||
message: '备份文件删除成功',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `备份文件删除失败: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/common/database/database.module.ts
Normal file
51
src/common/database/database.module.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { DatabaseBackupService } from './backup.service';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
|
||||
/**
|
||||
* 数据库模块 - 基础设施层
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/techniques/database
|
||||
* 对应 Java: MyBatis-Plus
|
||||
*
|
||||
* 职责:
|
||||
* - TypeORM 配置和连接管理
|
||||
* - 数据库备份和恢复服务
|
||||
* - 数据库相关的工具服务
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: configService.get('database.host', 'localhost'),
|
||||
port: configService.get('database.port', 3306),
|
||||
username: configService.get('database.username', 'root'),
|
||||
password: configService.get('database.password', 'root'),
|
||||
database: configService.get('database.database', 'wwjcloud'),
|
||||
entities: [
|
||||
__dirname + '/../**/*.entity{.ts,.js}',
|
||||
__dirname + '/../../config/**/*.entity{.ts,.js}',
|
||||
],
|
||||
synchronize: true, // 临时启用自动同步来创建表
|
||||
logging: configService.get('database.logging', false),
|
||||
timezone: '+08:00',
|
||||
charset: 'utf8mb4',
|
||||
extra: {
|
||||
connectionLimit: 10,
|
||||
acquireTimeout: 60000,
|
||||
timeout: 60000,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
UtilsModule,
|
||||
],
|
||||
providers: [DatabaseBackupService],
|
||||
exports: [TypeOrmModule, DatabaseBackupService],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
31
src/common/event/event.module.ts
Normal file
31
src/common/event/event.module.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
|
||||
/**
|
||||
* 事件模块 - 基础设施层
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/techniques/events
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
EventEmitterModule.forRoot({
|
||||
// 设置为 true 时,将允许使用通配符
|
||||
wildcard: false,
|
||||
// 事件分隔符
|
||||
delimiter: '.',
|
||||
// 设置为 true 时,如果事件没有监听器将抛出错误
|
||||
newListener: false,
|
||||
// 设置为 true 时,当事件监听器被移除时将发出事件
|
||||
removeListener: false,
|
||||
// 最大监听器数量
|
||||
maxListeners: 10,
|
||||
// 设置为 true 时,将在内存中存储包装函数的名称
|
||||
verboseMemoryLeak: false,
|
||||
// 禁用在内存泄漏警告中打印堆栈跟踪
|
||||
ignoreErrors: false,
|
||||
}),
|
||||
],
|
||||
exports: [EventEmitterModule],
|
||||
})
|
||||
export class EventModule {}
|
||||
48
src/common/exception/base.exception.ts
Normal file
48
src/common/exception/base.exception.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 基础异常类
|
||||
* 基于 NestJS 异常处理实现
|
||||
* 与PHP/Java框架保持完全一致的异常格式
|
||||
*
|
||||
* PHP格式: {data, msg, code}
|
||||
* Java格式: {code, msg, data}
|
||||
* NestJS格式: {code, msg, data} (与Java一致)
|
||||
*/
|
||||
export abstract class BaseException extends Error {
|
||||
public readonly statusCode: number;
|
||||
public readonly errorCode: string;
|
||||
public readonly timestamp: string;
|
||||
public readonly path?: string;
|
||||
public readonly details?: any;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
statusCode: number = 500,
|
||||
errorCode: string = 'INTERNAL_ERROR',
|
||||
details?: any,
|
||||
) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.statusCode = statusCode;
|
||||
this.errorCode = errorCode;
|
||||
this.timestamp = new Date().toISOString();
|
||||
this.details = details;
|
||||
|
||||
// 确保堆栈跟踪正确
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应格式
|
||||
* 与PHP/Java框架基本一致,添加timestamp字段
|
||||
*/
|
||||
toResponse() {
|
||||
return {
|
||||
code: 0, // 失败状态码
|
||||
msg: this.message, // 错误消息
|
||||
data: null, // 无数据
|
||||
timestamp: this.timestamp, // 时间戳
|
||||
};
|
||||
}
|
||||
}
|
||||
105
src/common/exception/business.exception.ts
Normal file
105
src/common/exception/business.exception.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { BaseException } from './base.exception';
|
||||
|
||||
/**
|
||||
* 业务异常类
|
||||
* 对应 Java: BusinessException
|
||||
*/
|
||||
export class BusinessException extends BaseException {
|
||||
constructor(
|
||||
message: string,
|
||||
errorCode: string = 'BUSINESS_ERROR',
|
||||
details?: any,
|
||||
) {
|
||||
super(message, 400, errorCode, details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证异常类
|
||||
* 对应 Java: AuthException
|
||||
*/
|
||||
export class AuthException extends BaseException {
|
||||
constructor(message: string = '认证失败', details?: any) {
|
||||
super(message, 401, 'AUTH_ERROR', details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权异常类
|
||||
* 对应 Java: AuthorizationException
|
||||
*/
|
||||
export class AuthorizationException extends BaseException {
|
||||
constructor(message: string = '权限不足', details?: any) {
|
||||
super(message, 403, 'AUTHORIZATION_ERROR', details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源未找到异常类
|
||||
* 对应 Java: ResourceNotFoundException
|
||||
*/
|
||||
export class ResourceNotFoundException extends BaseException {
|
||||
constructor(message: string = '资源未找到', details?: any) {
|
||||
super(message, 404, 'RESOURCE_NOT_FOUND', details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证异常类
|
||||
* 对应 Java: ValidationException
|
||||
*/
|
||||
export class ValidationException extends BaseException {
|
||||
constructor(message: string = '参数验证失败', details?: any) {
|
||||
super(message, 422, 'VALIDATION_ERROR', details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲突异常类
|
||||
* 对应 Java: ConflictException
|
||||
*/
|
||||
export class ConflictException extends BaseException {
|
||||
constructor(message: string = '资源冲突', details?: any) {
|
||||
super(message, 409, 'CONFLICT_ERROR', details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 限流异常类
|
||||
* 对应 Java: RateLimitException
|
||||
*/
|
||||
export class RateLimitException extends BaseException {
|
||||
constructor(message: string = '请求过于频繁', details?: any) {
|
||||
super(message, 429, 'RATE_LIMIT_ERROR', details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统异常类
|
||||
* 对应 Java: SystemException
|
||||
*/
|
||||
export class SystemException extends BaseException {
|
||||
constructor(message: string = '系统内部错误', details?: any) {
|
||||
super(message, 500, 'SYSTEM_ERROR', details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库异常类
|
||||
* 对应 Java: DatabaseException
|
||||
*/
|
||||
export class DatabaseException extends BaseException {
|
||||
constructor(message: string = '数据库操作失败', details?: any) {
|
||||
super(message, 500, 'DATABASE_ERROR', details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络异常类
|
||||
* 对应 Java: NetworkException
|
||||
*/
|
||||
export class NetworkException extends BaseException {
|
||||
constructor(message: string = '网络连接失败', details?: any) {
|
||||
super(message, 503, 'NETWORK_ERROR', details);
|
||||
}
|
||||
}
|
||||
97
src/common/exception/exception.filter.ts
Normal file
97
src/common/exception/exception.filter.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { BaseException } from './base.exception';
|
||||
import { BusinessException } from './business.exception';
|
||||
|
||||
/**
|
||||
* 全局异常过滤器
|
||||
* 基于 NestJS 异常处理实现
|
||||
* 参考: https://docs.nestjs.cn/exception-filters
|
||||
* 对应 Java: GlobalExceptionHandler
|
||||
*/
|
||||
@Catch()
|
||||
export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(GlobalExceptionFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let status: number;
|
||||
let errorResponse: any;
|
||||
|
||||
if (exception instanceof BaseException) {
|
||||
// 自定义业务异常
|
||||
status = exception.statusCode;
|
||||
errorResponse = exception.toResponse();
|
||||
errorResponse.error.path = request.url;
|
||||
} else if (exception instanceof HttpException) {
|
||||
// NestJS HTTP 异常
|
||||
status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
if (typeof exceptionResponse === 'string') {
|
||||
errorResponse = {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'HTTP_ERROR',
|
||||
message: exceptionResponse,
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
errorResponse = {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'HTTP_ERROR',
|
||||
message: (exceptionResponse as any).message || 'HTTP异常',
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
details: exceptionResponse,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 未知异常
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
errorResponse = {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: '系统内部错误',
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
this.logger.error(
|
||||
`Exception caught: ${exception instanceof Error ? exception.message : 'Unknown error'}`,
|
||||
exception instanceof Error ? exception.stack : undefined,
|
||||
{
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
ip: request.ip,
|
||||
userAgent: request.get('User-Agent'),
|
||||
status,
|
||||
errorCode: errorResponse.error.code,
|
||||
},
|
||||
);
|
||||
|
||||
// 发送响应
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
}
|
||||
352
src/common/exception/exception.interface.ts
Normal file
352
src/common/exception/exception.interface.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* 异常接口定义
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/exception-filters
|
||||
* 对应 Java: 异常抽象
|
||||
*/
|
||||
export interface ExceptionInterface {
|
||||
/**
|
||||
* 处理异常
|
||||
* @param exception 异常对象
|
||||
* @param context 上下文
|
||||
* @returns 处理结果
|
||||
*/
|
||||
handle(exception: any, context?: any): Promise<ExceptionResult>;
|
||||
|
||||
/**
|
||||
* 记录异常
|
||||
* @param exception 异常对象
|
||||
* @param context 上下文
|
||||
*/
|
||||
log(exception: any, context?: any): Promise<void>;
|
||||
|
||||
/**
|
||||
* 格式化异常响应
|
||||
* @param exception 异常对象
|
||||
* @param context 上下文
|
||||
* @returns 格式化后的响应
|
||||
*/
|
||||
format(exception: any, context?: any): ExceptionResponse;
|
||||
|
||||
/**
|
||||
* 判断异常类型
|
||||
* @param exception 异常对象
|
||||
* @returns 异常类型
|
||||
*/
|
||||
getType(exception: any): ExceptionType;
|
||||
|
||||
/**
|
||||
* 获取异常严重程度
|
||||
* @param exception 异常对象
|
||||
* @returns 严重程度
|
||||
*/
|
||||
getSeverity(exception: any): ExceptionSeverity;
|
||||
|
||||
/**
|
||||
* 判断是否应该记录
|
||||
* @param exception 异常对象
|
||||
* @returns 是否应该记录
|
||||
*/
|
||||
shouldLog(exception: any): boolean;
|
||||
|
||||
/**
|
||||
* 判断是否应该上报
|
||||
* @param exception 异常对象
|
||||
* @returns 是否应该上报
|
||||
*/
|
||||
shouldReport(exception: any): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常处理结果
|
||||
*/
|
||||
export interface ExceptionResult {
|
||||
success: boolean;
|
||||
response: ExceptionResponse;
|
||||
logged: boolean;
|
||||
reported: boolean;
|
||||
handled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常响应
|
||||
*/
|
||||
export interface ExceptionResponse {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
error: string;
|
||||
timestamp: string;
|
||||
path?: string;
|
||||
method?: string;
|
||||
traceId?: string;
|
||||
spanId?: string;
|
||||
correlationId?: string;
|
||||
details?: any;
|
||||
code?: string;
|
||||
type?: ExceptionType;
|
||||
severity?: ExceptionSeverity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常类型
|
||||
*/
|
||||
export enum ExceptionType {
|
||||
VALIDATION = 'validation',
|
||||
AUTHENTICATION = 'authentication',
|
||||
AUTHORIZATION = 'authorization',
|
||||
NOT_FOUND = 'not_found',
|
||||
CONFLICT = 'conflict',
|
||||
BAD_REQUEST = 'bad_request',
|
||||
INTERNAL_SERVER_ERROR = 'internal_server_error',
|
||||
SERVICE_UNAVAILABLE = 'service_unavailable',
|
||||
TIMEOUT = 'timeout',
|
||||
RATE_LIMIT = 'rate_limit',
|
||||
BUSINESS = 'business',
|
||||
SYSTEM = 'system',
|
||||
NETWORK = 'network',
|
||||
DATABASE = 'database',
|
||||
CACHE = 'cache',
|
||||
EXTERNAL_API = 'external_api',
|
||||
UNKNOWN = 'unknown',
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常严重程度
|
||||
*/
|
||||
export enum ExceptionSeverity {
|
||||
LOW = 'low',
|
||||
MEDIUM = 'medium',
|
||||
HIGH = 'high',
|
||||
CRITICAL = 'critical',
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常上下文
|
||||
*/
|
||||
export interface ExceptionContext {
|
||||
request?: {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
body?: any;
|
||||
query?: Record<string, any>;
|
||||
params?: Record<string, any>;
|
||||
ip?: string;
|
||||
userAgent?: string;
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
response?: {
|
||||
statusCode: number;
|
||||
headers: Record<string, string>;
|
||||
body?: any;
|
||||
};
|
||||
user?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
permissions?: string[];
|
||||
};
|
||||
environment?: {
|
||||
nodeEnv: string;
|
||||
version: string;
|
||||
hostname: string;
|
||||
pid: number;
|
||||
};
|
||||
trace?: {
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
correlationId: string;
|
||||
};
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常配置
|
||||
*/
|
||||
export interface ExceptionConfig {
|
||||
enabled: boolean;
|
||||
logLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
reportLevel: 'error' | 'fatal';
|
||||
includeStackTrace: boolean;
|
||||
includeRequest: boolean;
|
||||
includeResponse: boolean;
|
||||
includeUser: boolean;
|
||||
includeEnvironment: boolean;
|
||||
sanitizeData: boolean;
|
||||
maxMessageLength: number;
|
||||
maxStackTraceLength: number;
|
||||
rateLimit: {
|
||||
enabled: boolean;
|
||||
maxRequests: number;
|
||||
windowMs: number;
|
||||
};
|
||||
reporting: {
|
||||
enabled: boolean;
|
||||
providers: string[];
|
||||
filters: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常装饰器选项
|
||||
*/
|
||||
export interface ExceptionOptions {
|
||||
/**
|
||||
* 异常类型
|
||||
*/
|
||||
type?: ExceptionType;
|
||||
|
||||
/**
|
||||
* 严重程度
|
||||
*/
|
||||
severity?: ExceptionSeverity;
|
||||
|
||||
/**
|
||||
* 是否记录
|
||||
*/
|
||||
log?: boolean;
|
||||
|
||||
/**
|
||||
* 是否上报
|
||||
*/
|
||||
report?: boolean;
|
||||
|
||||
/**
|
||||
* 自定义消息
|
||||
*/
|
||||
message?: string;
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
statusCode?: number;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
code?: string;
|
||||
|
||||
/**
|
||||
* 是否包含堆栈跟踪
|
||||
*/
|
||||
includeStackTrace?: boolean;
|
||||
|
||||
/**
|
||||
* 是否包含请求信息
|
||||
*/
|
||||
includeRequest?: boolean;
|
||||
|
||||
/**
|
||||
* 是否包含响应信息
|
||||
*/
|
||||
includeResponse?: boolean;
|
||||
|
||||
/**
|
||||
* 是否包含用户信息
|
||||
*/
|
||||
includeUser?: boolean;
|
||||
|
||||
/**
|
||||
* 是否包含环境信息
|
||||
*/
|
||||
includeEnvironment?: boolean;
|
||||
|
||||
/**
|
||||
* 是否清理敏感数据
|
||||
*/
|
||||
sanitizeData?: boolean;
|
||||
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常过滤器接口
|
||||
*/
|
||||
export interface ExceptionFilterInterface {
|
||||
/**
|
||||
* 判断是否应该处理该异常
|
||||
* @param exception 异常对象
|
||||
* @returns 是否应该处理
|
||||
*/
|
||||
shouldHandle(exception: any): boolean;
|
||||
|
||||
/**
|
||||
* 处理异常
|
||||
* @param exception 异常对象
|
||||
* @param context 上下文
|
||||
* @returns 处理结果
|
||||
*/
|
||||
handle(exception: any, context?: any): Promise<ExceptionResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常上报接口
|
||||
*/
|
||||
export interface ExceptionReporterInterface {
|
||||
/**
|
||||
* 上报异常
|
||||
* @param exception 异常对象
|
||||
* @param context 上下文
|
||||
* @returns 上报结果
|
||||
*/
|
||||
report(exception: any, context?: any): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 批量上报异常
|
||||
* @param exceptions 异常数组
|
||||
* @param context 上下文
|
||||
* @returns 上报结果
|
||||
*/
|
||||
reportBatch(
|
||||
exceptions: Array<{ exception: any; context?: any }>,
|
||||
): Promise<boolean[]>;
|
||||
|
||||
/**
|
||||
* 检查是否应该上报
|
||||
* @param exception 异常对象
|
||||
* @returns 是否应该上报
|
||||
*/
|
||||
shouldReport(exception: any): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常统计接口
|
||||
*/
|
||||
export interface ExceptionStatsInterface {
|
||||
/**
|
||||
* 记录异常统计
|
||||
* @param exception 异常对象
|
||||
* @param context 上下文
|
||||
*/
|
||||
record(exception: any, context?: any): void;
|
||||
|
||||
/**
|
||||
* 获取异常统计
|
||||
* @param timeRange 时间范围
|
||||
* @returns 统计信息
|
||||
*/
|
||||
getStats(timeRange?: { start: Date; end: Date }): ExceptionStats;
|
||||
|
||||
/**
|
||||
* 重置统计
|
||||
*/
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常统计信息
|
||||
*/
|
||||
export interface ExceptionStats {
|
||||
total: number;
|
||||
byType: Record<ExceptionType, number>;
|
||||
bySeverity: Record<ExceptionSeverity, number>;
|
||||
byTime: Array<{ time: Date; count: number }>;
|
||||
topExceptions: Array<{ message: string; count: number; lastOccurred: Date }>;
|
||||
rate: number; // 异常率
|
||||
trend: 'increasing' | 'decreasing' | 'stable';
|
||||
}
|
||||
94
src/common/exception/exception.module.ts
Normal file
94
src/common/exception/exception.module.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ExceptionService } from './exception.service';
|
||||
import {
|
||||
ExceptionFilterInterface,
|
||||
ExceptionReporterInterface,
|
||||
ExceptionStatsInterface,
|
||||
} from './exception.interface';
|
||||
|
||||
/**
|
||||
* 异常处理模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/exception-filters
|
||||
* 对应 Java: 异常处理配置
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: 'EXCEPTION_FILTER_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 这里会根据配置选择具体的异常过滤器实现
|
||||
// 默认使用全局异常过滤器
|
||||
return {
|
||||
async catch(exception: any, context: any): Promise<void> {
|
||||
console.error('[EXCEPTION_FILTER]', exception.message, context);
|
||||
},
|
||||
async handle(exception: any, context: any): Promise<any> {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: exception.message || 'Internal server error',
|
||||
timestamp: new Date().toISOString(),
|
||||
path: context?.request?.url,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: 'EXCEPTION_REPORTER_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 异常上报实现
|
||||
return {
|
||||
async report(exception: any, context?: any): Promise<void> {
|
||||
console.error('[EXCEPTION_REPORTER]', exception.message, context);
|
||||
},
|
||||
async reportError(error: Error, context?: any): Promise<void> {
|
||||
console.error(
|
||||
'[EXCEPTION_REPORTER]',
|
||||
error.message,
|
||||
error.stack,
|
||||
context,
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: 'EXCEPTION_STATS_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 异常统计实现
|
||||
const stats = {
|
||||
total: 0,
|
||||
byType: new Map<string, number>(),
|
||||
byTime: new Map<string, number>(),
|
||||
};
|
||||
return {
|
||||
async record(exception: any): Promise<void> {
|
||||
stats.total++;
|
||||
const type = exception.constructor.name;
|
||||
stats.byType.set(type, (stats.byType.get(type) || 0) + 1);
|
||||
|
||||
const hour = new Date().toISOString().substring(0, 13);
|
||||
stats.byTime.set(hour, (stats.byTime.get(hour) || 0) + 1);
|
||||
},
|
||||
async getStats(): Promise<any> {
|
||||
return {
|
||||
total: stats.total,
|
||||
byType: Object.fromEntries(stats.byType),
|
||||
byTime: Object.fromEntries(stats.byTime),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
ExceptionService,
|
||||
],
|
||||
exports: [ExceptionService],
|
||||
})
|
||||
export class ExceptionModule {}
|
||||
537
src/common/exception/exception.service.ts
Normal file
537
src/common/exception/exception.service.ts
Normal file
@@ -0,0 +1,537 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import type {
|
||||
ExceptionInterface,
|
||||
ExceptionResult,
|
||||
ExceptionResponse,
|
||||
ExceptionContext,
|
||||
ExceptionOptions,
|
||||
ExceptionFilterInterface,
|
||||
ExceptionReporterInterface,
|
||||
ExceptionStatsInterface,
|
||||
} from './exception.interface';
|
||||
import { ExceptionType, ExceptionSeverity } from './exception.interface';
|
||||
|
||||
/**
|
||||
* 异常服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/exception-filters
|
||||
* 对应 Java: 异常处理服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExceptionService implements ExceptionInterface {
|
||||
private readonly logger = new Logger(ExceptionService.name);
|
||||
|
||||
constructor(
|
||||
@Inject('EXCEPTION_FILTER_PROVIDER')
|
||||
private readonly exceptionFilter: ExceptionFilterInterface,
|
||||
@Inject('EXCEPTION_REPORTER_PROVIDER')
|
||||
private readonly exceptionReporter: ExceptionReporterInterface,
|
||||
@Inject('EXCEPTION_STATS_PROVIDER')
|
||||
private readonly exceptionStats: ExceptionStatsInterface,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 处理异常
|
||||
*/
|
||||
async handle(exception: any, context?: any): Promise<ExceptionResult> {
|
||||
try {
|
||||
// 记录异常统计
|
||||
this.exceptionStats.record(exception, context);
|
||||
|
||||
// 判断是否应该处理
|
||||
if (!this.exceptionFilter.shouldHandle(exception)) {
|
||||
return {
|
||||
success: false,
|
||||
response: this.format(exception, context),
|
||||
logged: false,
|
||||
reported: false,
|
||||
handled: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 处理异常
|
||||
const result = await this.exceptionFilter.handle(exception, context);
|
||||
|
||||
// 记录异常
|
||||
if (this.shouldLog(exception)) {
|
||||
await this.log(exception, context);
|
||||
result.logged = true;
|
||||
}
|
||||
|
||||
// 上报异常
|
||||
if (this.shouldReport(exception)) {
|
||||
const reported = await this.exceptionReporter.report(
|
||||
exception,
|
||||
context,
|
||||
);
|
||||
result.reported = reported;
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to handle exception', error);
|
||||
return {
|
||||
success: false,
|
||||
response: this.format(exception, context),
|
||||
logged: false,
|
||||
reported: false,
|
||||
handled: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录异常
|
||||
*/
|
||||
async log(exception: any, context?: any): Promise<void> {
|
||||
try {
|
||||
const type = this.getType(exception);
|
||||
const severity = this.getSeverity(exception);
|
||||
const message = this.getMessage(exception);
|
||||
const stack = this.getStackTrace(exception);
|
||||
|
||||
const logData = {
|
||||
type,
|
||||
severity,
|
||||
message,
|
||||
stack,
|
||||
context: this.sanitizeContext(context),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
switch (severity) {
|
||||
case ExceptionSeverity.CRITICAL:
|
||||
this.logger.fatal(`Exception: ${message}`, logData);
|
||||
break;
|
||||
case ExceptionSeverity.HIGH:
|
||||
this.logger.error(`Exception: ${message}`, logData);
|
||||
break;
|
||||
case ExceptionSeverity.MEDIUM:
|
||||
this.logger.warn(`Exception: ${message}`, logData);
|
||||
break;
|
||||
case ExceptionSeverity.LOW:
|
||||
this.logger.log(`Exception: ${message}`, logData);
|
||||
break;
|
||||
default:
|
||||
this.logger.debug(`Exception: ${message}`, logData);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to log exception', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化异常响应
|
||||
*/
|
||||
format(exception: any, context?: any): ExceptionResponse {
|
||||
const type = this.getType(exception);
|
||||
const severity = this.getSeverity(exception);
|
||||
const message = this.getMessage(exception);
|
||||
const statusCode = this.getStatusCode(exception);
|
||||
const code = this.getCode(exception);
|
||||
|
||||
const response: ExceptionResponse = {
|
||||
statusCode,
|
||||
message,
|
||||
error: this.getErrorName(exception),
|
||||
timestamp: new Date().toISOString(),
|
||||
type,
|
||||
severity,
|
||||
code,
|
||||
};
|
||||
|
||||
// 添加请求信息
|
||||
if (context?.request) {
|
||||
response.path = context.request.url;
|
||||
response.method = context.request.method;
|
||||
}
|
||||
|
||||
// 添加追踪信息
|
||||
if (context?.trace) {
|
||||
response.traceId = context.trace.traceId;
|
||||
response.spanId = context.trace.spanId;
|
||||
response.correlationId = context.trace.correlationId;
|
||||
}
|
||||
|
||||
// 添加详细信息
|
||||
if (this.shouldIncludeDetails(exception)) {
|
||||
response.details = this.getDetails(exception);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断异常类型
|
||||
*/
|
||||
getType(exception: any): ExceptionType {
|
||||
if (exception.name) {
|
||||
const name = exception.name.toLowerCase();
|
||||
|
||||
if (name.includes('validation') || name.includes('badrequest')) {
|
||||
return ExceptionType.VALIDATION;
|
||||
}
|
||||
if (name.includes('unauthorized') || name.includes('authentication')) {
|
||||
return ExceptionType.AUTHENTICATION;
|
||||
}
|
||||
if (name.includes('forbidden') || name.includes('authorization')) {
|
||||
return ExceptionType.AUTHORIZATION;
|
||||
}
|
||||
if (name.includes('notfound')) {
|
||||
return ExceptionType.NOT_FOUND;
|
||||
}
|
||||
if (name.includes('conflict')) {
|
||||
return ExceptionType.CONFLICT;
|
||||
}
|
||||
if (name.includes('timeout')) {
|
||||
return ExceptionType.TIMEOUT;
|
||||
}
|
||||
if (name.includes('ratelimit')) {
|
||||
return ExceptionType.RATE_LIMIT;
|
||||
}
|
||||
if (name.includes('database') || name.includes('db')) {
|
||||
return ExceptionType.DATABASE;
|
||||
}
|
||||
if (name.includes('cache')) {
|
||||
return ExceptionType.CACHE;
|
||||
}
|
||||
if (name.includes('network') || name.includes('connection')) {
|
||||
return ExceptionType.NETWORK;
|
||||
}
|
||||
if (name.includes('external') || name.includes('api')) {
|
||||
return ExceptionType.EXTERNAL_API;
|
||||
}
|
||||
if (name.includes('business')) {
|
||||
return ExceptionType.BUSINESS;
|
||||
}
|
||||
if (name.includes('system')) {
|
||||
return ExceptionType.SYSTEM;
|
||||
}
|
||||
}
|
||||
|
||||
if (exception.statusCode) {
|
||||
switch (exception.statusCode) {
|
||||
case 400:
|
||||
return ExceptionType.BAD_REQUEST;
|
||||
case 401:
|
||||
return ExceptionType.AUTHENTICATION;
|
||||
case 403:
|
||||
return ExceptionType.AUTHORIZATION;
|
||||
case 404:
|
||||
return ExceptionType.NOT_FOUND;
|
||||
case 409:
|
||||
return ExceptionType.CONFLICT;
|
||||
case 429:
|
||||
return ExceptionType.RATE_LIMIT;
|
||||
case 500:
|
||||
return ExceptionType.INTERNAL_SERVER_ERROR;
|
||||
case 503:
|
||||
return ExceptionType.SERVICE_UNAVAILABLE;
|
||||
default:
|
||||
return ExceptionType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
return ExceptionType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常严重程度
|
||||
*/
|
||||
getSeverity(exception: any): ExceptionSeverity {
|
||||
const type = this.getType(exception);
|
||||
|
||||
switch (type) {
|
||||
case ExceptionType.INTERNAL_SERVER_ERROR:
|
||||
case ExceptionType.SYSTEM:
|
||||
return ExceptionSeverity.CRITICAL;
|
||||
case ExceptionType.AUTHENTICATION:
|
||||
case ExceptionType.AUTHORIZATION:
|
||||
case ExceptionType.DATABASE:
|
||||
case ExceptionType.NETWORK:
|
||||
return ExceptionSeverity.HIGH;
|
||||
case ExceptionType.VALIDATION:
|
||||
case ExceptionType.BAD_REQUEST:
|
||||
case ExceptionType.CONFLICT:
|
||||
case ExceptionType.CACHE:
|
||||
case ExceptionType.EXTERNAL_API:
|
||||
return ExceptionSeverity.MEDIUM;
|
||||
case ExceptionType.NOT_FOUND:
|
||||
case ExceptionType.TIMEOUT:
|
||||
case ExceptionType.RATE_LIMIT:
|
||||
case ExceptionType.BUSINESS:
|
||||
return ExceptionSeverity.LOW;
|
||||
default:
|
||||
return ExceptionSeverity.MEDIUM;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该记录
|
||||
*/
|
||||
shouldLog(exception: any): boolean {
|
||||
const severity = this.getSeverity(exception);
|
||||
return severity !== ExceptionSeverity.LOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该上报
|
||||
*/
|
||||
shouldReport(exception: any): boolean {
|
||||
const severity = this.getSeverity(exception);
|
||||
return (
|
||||
severity === ExceptionSeverity.HIGH ||
|
||||
severity === ExceptionSeverity.CRITICAL
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取异常消息
|
||||
*/
|
||||
private getMessage(exception: any): string {
|
||||
if (exception.message) {
|
||||
return exception.message;
|
||||
}
|
||||
if (typeof exception === 'string') {
|
||||
return exception;
|
||||
}
|
||||
return 'Unknown error occurred';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常名称
|
||||
*/
|
||||
private getErrorName(exception: any): string {
|
||||
if (exception.name) {
|
||||
return exception.name;
|
||||
}
|
||||
if (exception.constructor?.name) {
|
||||
return exception.constructor.name;
|
||||
}
|
||||
return 'Error';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态码
|
||||
*/
|
||||
private getStatusCode(exception: any): number {
|
||||
if (exception.statusCode) {
|
||||
return exception.statusCode;
|
||||
}
|
||||
if (exception.status) {
|
||||
return exception.status;
|
||||
}
|
||||
|
||||
const type = this.getType(exception);
|
||||
switch (type) {
|
||||
case ExceptionType.VALIDATION:
|
||||
case ExceptionType.BAD_REQUEST:
|
||||
return 400;
|
||||
case ExceptionType.AUTHENTICATION:
|
||||
return 401;
|
||||
case ExceptionType.AUTHORIZATION:
|
||||
return 403;
|
||||
case ExceptionType.NOT_FOUND:
|
||||
return 404;
|
||||
case ExceptionType.CONFLICT:
|
||||
return 409;
|
||||
case ExceptionType.RATE_LIMIT:
|
||||
return 429;
|
||||
case ExceptionType.SERVICE_UNAVAILABLE:
|
||||
return 503;
|
||||
case ExceptionType.INTERNAL_SERVER_ERROR:
|
||||
default:
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误码
|
||||
*/
|
||||
private getCode(exception: any): string | undefined {
|
||||
if (exception.code) {
|
||||
return exception.code;
|
||||
}
|
||||
if (exception.errorCode) {
|
||||
return exception.errorCode;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取堆栈跟踪
|
||||
*/
|
||||
private getStackTrace(exception: any): string | undefined {
|
||||
if (exception.stack) {
|
||||
return exception.stack;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细信息
|
||||
*/
|
||||
private getDetails(exception: any): any {
|
||||
const details: any = {};
|
||||
|
||||
if (exception.details) {
|
||||
details.details = exception.details;
|
||||
}
|
||||
|
||||
if (exception.cause) {
|
||||
details.cause = exception.cause;
|
||||
}
|
||||
|
||||
if (exception.context) {
|
||||
details.context = exception.context;
|
||||
}
|
||||
|
||||
return Object.keys(details).length > 0 ? details : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该包含详细信息
|
||||
*/
|
||||
private shouldIncludeDetails(exception: any): boolean {
|
||||
const severity = this.getSeverity(exception);
|
||||
return (
|
||||
severity === ExceptionSeverity.HIGH ||
|
||||
severity === ExceptionSeverity.CRITICAL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理上下文
|
||||
*/
|
||||
private sanitizeContext(context: any): any {
|
||||
if (!context) return context;
|
||||
|
||||
const sanitized = { ...context };
|
||||
|
||||
// 清理敏感信息
|
||||
if (sanitized.request) {
|
||||
sanitized.request = this.sanitizeRequest(sanitized.request);
|
||||
}
|
||||
|
||||
if (sanitized.user) {
|
||||
sanitized.user = this.sanitizeUser(sanitized.user);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求信息
|
||||
*/
|
||||
private sanitizeRequest(request: any): any {
|
||||
if (!request) return request;
|
||||
|
||||
const sanitized = { ...request };
|
||||
|
||||
// 清理敏感头部
|
||||
if (sanitized.headers) {
|
||||
const sensitiveHeaders = [
|
||||
'authorization',
|
||||
'cookie',
|
||||
'x-api-key',
|
||||
'x-auth-token',
|
||||
];
|
||||
for (const header of sensitiveHeaders) {
|
||||
if (sanitized.headers[header]) {
|
||||
sanitized.headers[header] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理敏感请求体
|
||||
if (sanitized.body) {
|
||||
sanitized.body = this.sanitizeBody(sanitized.body);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理用户信息
|
||||
*/
|
||||
private sanitizeUser(user: any): any {
|
||||
if (!user) return user;
|
||||
|
||||
const sanitized = { ...user };
|
||||
|
||||
// 清理敏感用户信息
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key'];
|
||||
for (const field of sensitiveFields) {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求体
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body) return body;
|
||||
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
return this.sanitizeObject(parsed);
|
||||
} catch {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'object') {
|
||||
return this.sanitizeObject(body);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理对象
|
||||
*/
|
||||
private sanitizeObject(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
const sanitized = { ...obj };
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
// ==================== 装饰器支持 ====================
|
||||
|
||||
/**
|
||||
* 异常装饰器实现
|
||||
*/
|
||||
async handleWithOptions<T>(
|
||||
options: ExceptionOptions,
|
||||
fn: () => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
const result = await this.handle(error, options);
|
||||
|
||||
if (result.handled) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result.response as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/common/filters/validation-exception.filter.ts
Normal file
100
src/common/filters/validation-exception.filter.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { ExceptionFilter, Catch, ArgumentsHost, BadRequestException } from '@nestjs/common';
|
||||
import type { ILanguageService } from '@wwjCommon/language/language.interface';
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 验证异常过滤器
|
||||
* 将class-validator的错误转换为多语言消息
|
||||
* 符合NestJS规范的异常处理方式
|
||||
*/
|
||||
@Catch(BadRequestException)
|
||||
export class ValidationExceptionFilter implements ExceptionFilter {
|
||||
constructor(@Inject('ILanguageService') private readonly languageService: ILanguageService) {}
|
||||
|
||||
async catch(exception: BadRequestException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
const status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse() as any;
|
||||
|
||||
// 如果是验证错误,尝试转换为多语言消息
|
||||
if (exceptionResponse.message && Array.isArray(exceptionResponse.message)) {
|
||||
const translatedMessages = await this.translateValidationMessages(exceptionResponse.message);
|
||||
|
||||
response.status(status).json({
|
||||
code: 0,
|
||||
msg: translatedMessages.join('; '),
|
||||
data: null,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// 其他错误直接返回
|
||||
response.status(status).json({
|
||||
code: 0,
|
||||
msg: exceptionResponse.message || '请求参数错误',
|
||||
data: null,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译验证错误消息
|
||||
*/
|
||||
private async translateValidationMessages(messages: string[]): Promise<string[]> {
|
||||
const translatedMessages: string[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
try {
|
||||
// 尝试从验证错误消息中提取字段名和规则
|
||||
const fieldMatch = message.match(/^(\w+)\s+(.+)$/);
|
||||
if (fieldMatch) {
|
||||
const [, field, rule] = fieldMatch;
|
||||
|
||||
// 根据字段名和规则获取多语言消息
|
||||
let translatedMessage: string;
|
||||
|
||||
if (rule.includes('should not be empty')) {
|
||||
translatedMessage = await this.languageService.getValidateMessage(
|
||||
`validate_user.${field}_require`,
|
||||
{ attribute: field }
|
||||
);
|
||||
} else if (rule.includes('must be longer than')) {
|
||||
const minMatch = rule.match(/must be longer than (\d+)/);
|
||||
const min = minMatch ? minMatch[1] : '6';
|
||||
translatedMessage = await this.languageService.getValidateMessage(
|
||||
`validate_user.${field}_min`,
|
||||
{ attribute: field, min }
|
||||
);
|
||||
} else if (rule.includes('must be shorter than')) {
|
||||
const maxMatch = rule.match(/must be shorter than (\d+)/);
|
||||
const max = maxMatch ? maxMatch[1] : '20';
|
||||
translatedMessage = await this.languageService.getValidateMessage(
|
||||
`validate_user.${field}_max`,
|
||||
{ attribute: field, max }
|
||||
);
|
||||
} else if (rule.includes('must be an email')) {
|
||||
translatedMessage = await this.languageService.getValidateMessage(
|
||||
`validate_user.${field}_format`,
|
||||
{ attribute: field }
|
||||
);
|
||||
} else {
|
||||
// 默认使用原始消息
|
||||
translatedMessage = message;
|
||||
}
|
||||
|
||||
translatedMessages.push(translatedMessage);
|
||||
} else {
|
||||
translatedMessages.push(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果翻译失败,使用原始消息
|
||||
translatedMessages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
return translatedMessages;
|
||||
}
|
||||
}
|
||||
15
src/common/init/init.module.ts
Normal file
15
src/common/init/init.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { InitService } from './init.service';
|
||||
|
||||
/**
|
||||
* 初始化模块 - 基础设施层
|
||||
* 提供应用启动初始化和健康检查功能
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [InitService],
|
||||
exports: [InitService],
|
||||
})
|
||||
export class InitModule {}
|
||||
125
src/common/init/init.service.ts
Normal file
125
src/common/init/init.service.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* 初始化服务
|
||||
* 提供应用启动初始化和健康检查功能
|
||||
*/
|
||||
@Injectable()
|
||||
export class InitService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(InitService.name);
|
||||
private isHealthy = false;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 模块初始化
|
||||
*/
|
||||
async onModuleInit() {
|
||||
this.logger.log('Initializing application...');
|
||||
|
||||
try {
|
||||
await this.initializeServices();
|
||||
await this.performHealthCheck();
|
||||
|
||||
this.isHealthy = true;
|
||||
this.logger.log('Application initialized successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize application', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块销毁
|
||||
*/
|
||||
async onModuleDestroy() {
|
||||
this.logger.log('Shutting down application...');
|
||||
|
||||
try {
|
||||
await this.cleanup();
|
||||
this.logger.log('Application shutdown completed');
|
||||
} catch (error) {
|
||||
this.logger.error('Error during application shutdown', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务
|
||||
*/
|
||||
private async initializeServices() {
|
||||
// 这里可以初始化各种服务
|
||||
// 例如:数据库连接、Redis 连接、外部服务等
|
||||
this.logger.log('Initializing services...');
|
||||
|
||||
// 模拟初始化过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
this.logger.log('Services initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行健康检查
|
||||
*/
|
||||
private async performHealthCheck() {
|
||||
this.logger.log('Performing health check...');
|
||||
|
||||
const healthConfig = this.configService.get('health');
|
||||
|
||||
if (healthConfig.startupCheckEnabled) {
|
||||
// 这里可以检查各种依赖服务的健康状态
|
||||
// 例如:数据库、Redis、外部 API 等
|
||||
|
||||
const timeout = healthConfig.startupTimeoutMs || 5000;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 模拟健康检查
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (duration > timeout) {
|
||||
throw new Error(`Health check timeout after ${duration}ms`);
|
||||
}
|
||||
|
||||
this.logger.log(`Health check completed in ${duration}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private async cleanup() {
|
||||
// 这里可以清理各种资源
|
||||
// 例如:关闭数据库连接、清理缓存等
|
||||
this.logger.log('Cleaning up resources...');
|
||||
|
||||
// 模拟清理过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
this.logger.log('Resources cleaned up');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取健康状态
|
||||
*/
|
||||
getHealthStatus() {
|
||||
return {
|
||||
status: this.isHealthy ? 'healthy' : 'unhealthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否健康
|
||||
*/
|
||||
isApplicationHealthy(): boolean {
|
||||
return this.isHealthy;
|
||||
}
|
||||
}
|
||||
14
src/common/interceptors/interceptors.module.ts
Normal file
14
src/common/interceptors/interceptors.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RequestParameterInterceptor } from './request-parameter.interceptor';
|
||||
import { MethodCallInterceptor } from './method-call.interceptor';
|
||||
|
||||
/**
|
||||
* 拦截器模块
|
||||
* 基于 NestJS 实现 Java 风格的 AOP 切面
|
||||
* 对应 Java: Aspect 配置
|
||||
*/
|
||||
@Module({
|
||||
providers: [RequestParameterInterceptor, MethodCallInterceptor],
|
||||
exports: [RequestParameterInterceptor, MethodCallInterceptor],
|
||||
})
|
||||
export class InterceptorsModule {}
|
||||
53
src/common/interceptors/method-call.interceptor.ts
Normal file
53
src/common/interceptors/method-call.interceptor.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* 方法调用监控拦截器
|
||||
* 基于 NestJS 实现 Java 风格的 MethodCallStackAspect
|
||||
* 对应 Java: MethodCallStackAspect
|
||||
*/
|
||||
@Injectable()
|
||||
export class MethodCallInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(MethodCallInterceptor.name);
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const className = context.getClass().name;
|
||||
const handler = context.getHandler();
|
||||
const methodName = handler.name;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 记录方法调用开始
|
||||
this.logger.debug(`${className} -> ${methodName}() begin ->`);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: (data) => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// 记录方法调用结束
|
||||
this.logger.debug(
|
||||
`${className} -> ${methodName}() ended result => ${JSON.stringify(data)} (${duration}ms)`,
|
||||
);
|
||||
},
|
||||
error: (error) => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// 记录方法调用异常
|
||||
this.logger.error(
|
||||
`${className} -> ${methodName}() error => ${error.message} (${duration}ms)`,
|
||||
error.stack,
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
207
src/common/interceptors/request-parameter.interceptor.ts
Normal file
207
src/common/interceptors/request-parameter.interceptor.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* 请求参数处理拦截器
|
||||
* 基于 NestJS 实现 Java 风格的 ControllerRequestAspect
|
||||
* 对应 Java: ControllerRequestAspect
|
||||
*/
|
||||
@Injectable()
|
||||
export class RequestParameterInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(RequestParameterInterceptor.name);
|
||||
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const handler = context.getHandler();
|
||||
const className = context.getClass().name;
|
||||
const methodName = handler.name;
|
||||
|
||||
// 记录请求开始
|
||||
this.logger.debug(`Before: ${className} -> ${methodName}()`);
|
||||
|
||||
// 处理请求参数
|
||||
this.processRequestParameters(request);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap((data) => {
|
||||
// 记录请求结束
|
||||
this.logger.debug(
|
||||
`${className} -> ${methodName}() ended result => ${JSON.stringify(data)}`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求参数
|
||||
* @param request 请求对象
|
||||
*/
|
||||
private processRequestParameters(request: any): void {
|
||||
try {
|
||||
// 处理查询参数
|
||||
this.processQueryParameters(request);
|
||||
|
||||
// 处理请求体参数
|
||||
this.processBodyParameters(request);
|
||||
|
||||
// 处理路径参数
|
||||
this.processPathParameters(request);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to process request parameters: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询参数
|
||||
* @param request 请求对象
|
||||
*/
|
||||
private processQueryParameters(request: any): void {
|
||||
const query = request.query;
|
||||
if (!query || typeof query !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理数组参数
|
||||
const processedQuery = this.processArrayParameters(query);
|
||||
|
||||
// 下划线转驼峰
|
||||
const camelCaseQuery = this.toCamelCase(processedQuery);
|
||||
|
||||
// 更新请求查询参数
|
||||
request.query = camelCaseQuery;
|
||||
|
||||
this.logger.debug(
|
||||
`Processed query parameters: ${JSON.stringify(camelCaseQuery)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求体参数
|
||||
* @param request 请求对象
|
||||
*/
|
||||
private processBodyParameters(request: any): void {
|
||||
const body = request.body;
|
||||
if (!body || typeof body !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理数组参数
|
||||
const processedBody = this.processArrayParameters(body);
|
||||
|
||||
// 下划线转驼峰
|
||||
const camelCaseBody = this.toCamelCase(processedBody);
|
||||
|
||||
// 更新请求体参数
|
||||
request.body = camelCaseBody;
|
||||
|
||||
this.logger.debug(
|
||||
`Processed body parameters: ${JSON.stringify(camelCaseBody)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理路径参数
|
||||
* @param request 请求对象
|
||||
*/
|
||||
private processPathParameters(request: any): void {
|
||||
const params = request.params;
|
||||
if (!params || typeof params !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 下划线转驼峰
|
||||
const camelCaseParams = this.toCamelCase(params);
|
||||
|
||||
// 更新请求路径参数
|
||||
request.params = camelCaseParams;
|
||||
|
||||
this.logger.debug(
|
||||
`Processed path parameters: ${JSON.stringify(camelCaseParams)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理数组参数
|
||||
* @param params 参数对象
|
||||
* @returns 处理后的参数对象
|
||||
*/
|
||||
private processArrayParameters(params: any): any {
|
||||
if (!params || typeof params !== 'object') {
|
||||
return params;
|
||||
}
|
||||
|
||||
const processed: any = {};
|
||||
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
const value = params[key];
|
||||
|
||||
if (key.endsWith('[]')) {
|
||||
// 处理数组参数
|
||||
const newKey = key.replace('[]', '');
|
||||
if (Array.isArray(value)) {
|
||||
processed[newKey] = value;
|
||||
} else if (typeof value === 'string') {
|
||||
// 逗号分隔的字符串转换为数组
|
||||
processed[newKey] = value.split(',').map((item) => item.trim());
|
||||
} else {
|
||||
processed[newKey] = [value];
|
||||
}
|
||||
} else {
|
||||
processed[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
* @param obj 要转换的对象
|
||||
* @returns 转换后的对象
|
||||
*/
|
||||
private toCamelCase(obj: any): any {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => this.toCamelCase(item));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const result: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const camelKey = this.snakeToCamel(key);
|
||||
result[camelKey] = this.toCamelCase(obj[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
* @param str 下划线字符串
|
||||
* @returns 驼峰字符串
|
||||
*/
|
||||
private snakeToCamel(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
|
||||
}
|
||||
}
|
||||
116
src/common/language/language.example.ts
Normal file
116
src/common/language/language.example.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { LanguageService } from './language.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
async function bootstrap() {
|
||||
// 模拟 NestJS 应用启动和 LanguageUtils 实例化
|
||||
const mockConfigService = {
|
||||
get: (key: string, defaultValue: any) => {
|
||||
if (key === 'app.supportedLocales') return ['zh_CN', 'en_US'];
|
||||
if (key === 'app.defaultLanguage') return 'zh_CN';
|
||||
return defaultValue;
|
||||
},
|
||||
} as ConfigService;
|
||||
|
||||
const languageService = new LanguageService(mockConfigService);
|
||||
// 模拟初始化,实际由 NestJS 模块管理
|
||||
// await languageService.initializeLanguagePacks();
|
||||
|
||||
console.log('--- LanguageService 使用示例 ---');
|
||||
|
||||
// 1. 获取通用API消息 (默认模块: common, 默认类型: api)
|
||||
const commonApiMessage = await languageService.getApiMessage('success');
|
||||
console.log(`通用API消息 (success): ${commonApiMessage}`); // 预期: 操作成功
|
||||
|
||||
// 2. 获取通用字典数据 (默认模块: common, 类型: dict)
|
||||
const commonDictMessage = await languageService.getDictData('dict_user.status_on');
|
||||
console.log(`通用字典数据 (dict_user.status_on): ${commonDictMessage}`); // 预期: 正常
|
||||
|
||||
// 3. 获取通用验证器消息 (默认模块: common, 类型: validate)
|
||||
const commonValidateMessage = await languageService.getValidateMessage('validate_user.username_require');
|
||||
console.log(`通用验证器消息 (validate_user.username_require): ${commonValidateMessage}`); // 预期: 账号必须填写
|
||||
|
||||
// 4. 获取用户模块API消息 (模块: user, 类型: api)
|
||||
const userApiMessage = await languageService.getApiMessage('create_success', undefined, 'user');
|
||||
console.log(`用户API消息 (user.create_success): ${userApiMessage}`); // 预期: 用户创建成功
|
||||
|
||||
// 5. 获取用户模块字典数据 (模块: user, 类型: dict)
|
||||
const userDictMessage = await languageService.getDictData('user_type.admin', undefined, 'user');
|
||||
console.log(`用户字典数据 (user.user_type.admin): ${userDictMessage}`); // 预期: 管理员
|
||||
|
||||
// 6. 获取用户模块验证器消息 (模块: user, 类型: validate)
|
||||
const userValidateMessage = await languageService.getValidateMessage('email_format_error', undefined, 'user');
|
||||
console.log(`用户验证器消息 (user.email_format_error): ${userValidateMessage}`); // 预期: 邮箱格式不正确
|
||||
|
||||
// 7. 获取带参数的消息
|
||||
const paramMessage = await languageService.getApiMessage('user_error', { name: '张三' });
|
||||
console.log(`带参数消息 (user_error): ${paramMessage}`); // 预期: 账号或密码错误 (如果user_error在common/api.json中)
|
||||
|
||||
// 8. 批量获取消息
|
||||
const batchMessages = await languageService.getBatchMessages(['success', 'fail'], 'common', 'api');
|
||||
console.log('批量获取消息 (common.api):', batchMessages); // 预期: { success: '操作成功', fail: '操作失败' }
|
||||
|
||||
// 9. 切换语言并获取消息
|
||||
languageService.setLanguage('en_US');
|
||||
const enApiMessage = await languageService.getApiMessage('success');
|
||||
console.log(`英文API消息 (success): ${enApiMessage}`); // 预期: Operation successful
|
||||
|
||||
// 10. 重新加载语言包
|
||||
await languageService.reloadLanguagePack('zh_CN');
|
||||
const reloadedMessage = await languageService.getApiMessage('success');
|
||||
console.log(`重新加载后消息 (success): ${reloadedMessage}`); // 预期: 操作成功
|
||||
|
||||
// 11. 场景化验证示例
|
||||
console.log('\n--- 场景化验证示例 ---');
|
||||
|
||||
// 模拟用户数据
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
password: '123456',
|
||||
real_name: '测试用户',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
// 验证添加场景
|
||||
console.log('添加场景验证数据:', userData);
|
||||
|
||||
// 验证编辑场景
|
||||
console.log('编辑场景验证数据:', userData);
|
||||
|
||||
// 12. 分组验证消息示例
|
||||
console.log('\n--- 分组验证消息示例 ---');
|
||||
|
||||
const menuValidation = await languageService.getValidateMessage('validate_menu.menu_name_require');
|
||||
console.log('菜单验证消息:', menuValidation); // 预期: 菜单名称必须填写
|
||||
|
||||
const roleValidation = await languageService.getValidateMessage('validate_role.role_name_require');
|
||||
console.log('角色验证消息:', roleValidation); // 预期: 角色名称必须填写
|
||||
|
||||
const siteValidation = await languageService.getValidateMessage('validate_site.site_name_require');
|
||||
console.log('站点验证消息:', siteValidation); // 预期: 网站名称必须填写
|
||||
|
||||
// 13. 字典数据示例
|
||||
console.log('\n--- 字典数据示例 ---');
|
||||
|
||||
const appDict = await languageService.getDictData('dict_app.type_admin');
|
||||
console.log('应用类型字典:', appDict); // 预期: 平台管理端
|
||||
|
||||
const menuDict = await languageService.getDictData('dict_menu.type_list');
|
||||
console.log('菜单类型字典:', menuDict); // 预期: 目录
|
||||
|
||||
const payDict = await languageService.getDictData('dict_pay.type_wechatpay');
|
||||
console.log('支付类型字典:', payDict); // 预期: 微信支付
|
||||
|
||||
// 14. 参数替换示例
|
||||
console.log('\n--- 参数替换示例 ---');
|
||||
|
||||
const paramMessage1 = await languageService.getValidateMessage('common.minLength', { min: 6 });
|
||||
console.log('最小长度验证:', paramMessage1); // 预期: 长度不能少于6个字符
|
||||
|
||||
const paramMessage2 = await languageService.getValidateMessage('common.maxLength', { max: 20 });
|
||||
console.log('最大长度验证:', paramMessage2); // 预期: 长度不能超过20个字符
|
||||
|
||||
const paramMessage3 = await languageService.getValidateMessage('common.between', { min: 1, max: 100 });
|
||||
console.log('范围验证:', paramMessage3); // 预期: 必须在1到100之间
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
94
src/common/language/language.interface.ts
Normal file
94
src/common/language/language.interface.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 语言服务接口
|
||||
* 符合NestJS规范的接口定义
|
||||
* 对应PHP: think\Lang 接口
|
||||
* 对应Java: MessageSource 接口
|
||||
*/
|
||||
export interface ILanguageService {
|
||||
/**
|
||||
* 获取消息
|
||||
* @param key 消息键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param type 类型 (api|dict|validate)
|
||||
* @param language 语言
|
||||
* @returns 消息内容
|
||||
*/
|
||||
getMessage(key: string, args?: any, module?: string, type?: string, language?: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* 获取API消息
|
||||
* @param key 消息键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param language 语言
|
||||
* @returns API消息
|
||||
*/
|
||||
getApiMessage(key: string, args?: any, module?: string, language?: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
* @param key 字典键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param language 语言
|
||||
* @returns 字典数据
|
||||
*/
|
||||
getDictData(key: string, args?: any, module?: string, language?: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* 获取验证器消息
|
||||
* @param key 验证器键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param language 语言
|
||||
* @returns 验证器消息
|
||||
*/
|
||||
getValidateMessage(key: string, args?: any, module?: string, language?: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* 批量获取消息
|
||||
* @param keys 消息键数组
|
||||
* @param module 模块名
|
||||
* @param type 类型
|
||||
* @param language 语言
|
||||
* @returns 消息对象
|
||||
*/
|
||||
getBatchMessages(keys: string[], module?: string, type?: string, language?: string): Promise<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
* @returns 当前语言
|
||||
*/
|
||||
getCurrentLanguage(): string;
|
||||
|
||||
/**
|
||||
* 设置语言
|
||||
* @param language 语言代码
|
||||
*/
|
||||
setLanguage(language: string): void;
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
* @returns 支持的语言列表
|
||||
*/
|
||||
getSupportedLanguages(): string[];
|
||||
|
||||
/**
|
||||
* 检查语言是否支持
|
||||
* @param language 语言代码
|
||||
* @returns 是否支持
|
||||
*/
|
||||
isLanguageSupported(language: string): boolean;
|
||||
|
||||
/**
|
||||
* 重新加载语言包
|
||||
* @param language 语言代码
|
||||
*/
|
||||
reloadLanguagePack(language: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 重新加载所有语言包
|
||||
*/
|
||||
reloadAllLanguagePacks(): Promise<void>;
|
||||
}
|
||||
29
src/common/language/language.module.ts
Normal file
29
src/common/language/language.module.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { LanguageService } from './language.service';
|
||||
import { ILanguageService } from './language.interface';
|
||||
|
||||
/**
|
||||
* 语言模块
|
||||
* 符合NestJS规范的多语言支持
|
||||
*
|
||||
* 功能:
|
||||
* 1. 多语言支持 (API消息、字典数据、验证器消息)
|
||||
* 2. 模块化语言包 (按模块、类型、语言组织)
|
||||
* 3. 热重载支持 (开发环境)
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在需要多语言的模块中导入
|
||||
* 2. 通过依赖注入使用ILanguageService接口
|
||||
* 3. 配合ValidationExceptionFilter处理多语言验证错误
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'ILanguageService',
|
||||
useClass: LanguageService,
|
||||
},
|
||||
],
|
||||
exports: ['ILanguageService'],
|
||||
})
|
||||
export class LanguageModule {}
|
||||
385
src/common/language/language.service.ts
Normal file
385
src/common/language/language.service.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { ILanguageService } from './language.interface';
|
||||
|
||||
/**
|
||||
* 语言服务
|
||||
* 基于 NestJS 实现模块化多语言支持
|
||||
* 符合NestJS规范的服务层实现
|
||||
* 对应PHP: think\Lang 类
|
||||
* 对应Java: MessageSource 实现
|
||||
*/
|
||||
@Injectable()
|
||||
export class LanguageService implements ILanguageService {
|
||||
private readonly logger = new Logger(LanguageService.name);
|
||||
private readonly languageCache = new Map<string, Map<string, any>>();
|
||||
private readonly moduleCache = new Map<string, Set<string>>(); // 记录已加载的模块
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.initializeLanguagePacks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息
|
||||
* @param key 消息键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param type 类型 (api|dict|validate)
|
||||
* @param language 语言
|
||||
* @returns 消息内容
|
||||
*/
|
||||
async getMessage(key: string, args?: any, module: string = 'common', type: string = 'api', language?: string): Promise<string> {
|
||||
try {
|
||||
const currentLanguage = language || this.getCurrentLanguage();
|
||||
const cacheKey = `${type}.${module}.${key}`;
|
||||
|
||||
// 确保模块已加载
|
||||
await this.ensureModuleLoaded(module, type, currentLanguage);
|
||||
|
||||
const message = this.getMessageFromCache(cacheKey, currentLanguage);
|
||||
|
||||
if (message && message !== key) {
|
||||
// 支持参数替换
|
||||
if (args && typeof args === 'object') {
|
||||
return this.replaceMessageParams(message, args);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
this.logger.warn(`未找到消息: ${key} (模块: ${module}, 类型: ${type}, 语言: ${currentLanguage})`);
|
||||
return key;
|
||||
} catch (error) {
|
||||
this.logger.warn(`获取消息失败: ${key}`, error);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息(实例方法)
|
||||
* @param key 消息键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param type 类型
|
||||
* @param language 语言
|
||||
* @returns 消息内容
|
||||
*/
|
||||
async get(key: string, args?: any, module: string = 'common', type: string = 'api', language?: string): Promise<string> {
|
||||
return await this.getMessage(key, args, module, type, language);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化语言包
|
||||
* 只加载通用语言包,其他模块按需加载
|
||||
*/
|
||||
private async initializeLanguagePacks(): Promise<void> {
|
||||
try {
|
||||
const supportedLanguages = this.getSupportedLanguages();
|
||||
for (const language of supportedLanguages) {
|
||||
// 只加载通用模块
|
||||
await this.loadModuleLanguagePack('common', 'api', language);
|
||||
await this.loadModuleLanguagePack('common', 'dict', language);
|
||||
await this.loadModuleLanguagePack('common', 'validate', language);
|
||||
}
|
||||
this.logger.log('通用语言包初始化完成');
|
||||
} catch (error) {
|
||||
this.logger.error('语言包初始化失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保模块已加载
|
||||
* @param module 模块名
|
||||
* @param type 类型
|
||||
* @param language 语言
|
||||
*/
|
||||
private async ensureModuleLoaded(module: string, type: string, language: string): Promise<void> {
|
||||
const moduleKey = `${module}.${type}`;
|
||||
const languageKey = `${language}.${moduleKey}`;
|
||||
|
||||
if (!this.moduleCache.has(language) || !this.moduleCache.get(language)!.has(moduleKey)) {
|
||||
await this.loadModuleLanguagePack(module, type, language);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模块语言包
|
||||
* @param module 模块名
|
||||
* @param type 类型 (api|dict|validate)
|
||||
* @param language 语言
|
||||
*/
|
||||
private async loadModuleLanguagePack(module: string, type: string, language: string): Promise<void> {
|
||||
try {
|
||||
const langDir = path.join(process.cwd(), 'src', 'lang', language);
|
||||
const languagePack = new Map<string, any>();
|
||||
|
||||
// 1. 加载通用语言包
|
||||
if (module !== 'common') {
|
||||
await this.loadCommonLanguagePack(langDir, type, languagePack);
|
||||
}
|
||||
|
||||
// 2. 加载模块语言包
|
||||
await this.loadModuleSpecificPack(langDir, module, type, languagePack);
|
||||
|
||||
// 3. 加载插件语言包
|
||||
await this.loadAddonLanguagePacks(langDir, type, languagePack);
|
||||
|
||||
// 4. 缓存语言包
|
||||
if (!this.languageCache.has(language)) {
|
||||
this.languageCache.set(language, new Map());
|
||||
}
|
||||
|
||||
const languageCache = this.languageCache.get(language)!;
|
||||
for (const [key, value] of languagePack) {
|
||||
languageCache.set(key, value);
|
||||
}
|
||||
|
||||
// 5. 记录已加载的模块
|
||||
if (!this.moduleCache.has(language)) {
|
||||
this.moduleCache.set(language, new Set());
|
||||
}
|
||||
this.moduleCache.get(language)!.add(`${module}.${type}`);
|
||||
|
||||
this.logger.log(`模块语言包加载完成: ${module}.${type} (${language})`);
|
||||
} catch (error) {
|
||||
this.logger.error(`加载模块语言包失败: ${module}.${type} (${language})`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载通用语言包
|
||||
*/
|
||||
private async loadCommonLanguagePack(langDir: string, type: string, languagePack: Map<string, any>): Promise<void> {
|
||||
const commonDir = path.join(langDir, 'common');
|
||||
const filePath = path.join(commonDir, `${type}.json`);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
// 合并到语言包,添加前缀
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
languagePack.set(`${type}.common.${key}`, value);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`加载通用语言包失败: ${type}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模块特定语言包
|
||||
*/
|
||||
private async loadModuleSpecificPack(langDir: string, module: string, type: string, languagePack: Map<string, any>): Promise<void> {
|
||||
const moduleDir = path.join(langDir, module);
|
||||
const filePath = path.join(moduleDir, `${type}.json`);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
// 合并到语言包,添加前缀
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
languagePack.set(`${type}.${module}.${key}`, value);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`加载模块语言包失败: ${module}.${type}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载插件语言包
|
||||
*/
|
||||
private async loadAddonLanguagePacks(langDir: string, type: string, languagePack: Map<string, any>): Promise<void> {
|
||||
const addonsDir = path.join(langDir, 'addons');
|
||||
|
||||
try {
|
||||
const addonDirs = await fs.readdir(addonsDir);
|
||||
for (const addonDir of addonDirs) {
|
||||
const addonPath = path.join(addonsDir, addonDir);
|
||||
const stat = await fs.stat(addonPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const filePath = path.join(addonPath, `${type}.json`);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
// 合并到语言包,添加前缀
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
languagePack.set(`${type}.addon.${addonDir}.${key}`, value);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`加载插件语言包失败: ${addonDir}.${type}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`扫描插件语言包失败: ${type}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并语言数据
|
||||
*/
|
||||
private mergeLanguageData(target: Map<string, any>, source: Record<string, any>): void {
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
target.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存获取消息
|
||||
*/
|
||||
private getMessageFromCache(key: string, language: string): string {
|
||||
const languagePack = this.languageCache.get(language);
|
||||
if (languagePack && languagePack.has(key)) {
|
||||
return languagePack.get(key);
|
||||
}
|
||||
|
||||
return key; // 未找到,返回key作为fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换消息参数
|
||||
* 对应 Java: MessageFormat.format()
|
||||
*/
|
||||
private replaceMessageParams(message: string, args: Record<string, any>): string {
|
||||
let result = message;
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
const placeholder = `{${key}}`;
|
||||
result = result.replace(new RegExp(placeholder, 'g'), String(value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
* @returns 当前语言
|
||||
*/
|
||||
getCurrentLanguage(): string {
|
||||
return this.getDefaultLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置语言
|
||||
* @param language 语言代码
|
||||
*/
|
||||
setLanguage(language: string): void {
|
||||
this.logger.log(`设置语言: ${language}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
* @returns 支持的语言列表
|
||||
*/
|
||||
getSupportedLanguages(): string[] {
|
||||
return this.configService.get('app.supportedLocales', ['zh_CN', 'en_US']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认语言
|
||||
* @returns 默认语言
|
||||
*/
|
||||
getDefaultLanguage(): string {
|
||||
return this.configService.get('app.defaultLanguage', 'zh_CN');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查语言是否支持
|
||||
* @param language 语言代码
|
||||
* @returns 是否支持
|
||||
*/
|
||||
isLanguageSupported(language: string): boolean {
|
||||
const supportedLanguages = this.getSupportedLanguages();
|
||||
return supportedLanguages.includes(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载语言包
|
||||
*/
|
||||
async reloadLanguagePack(language: string): Promise<void> {
|
||||
try {
|
||||
// 清除该语言的所有缓存
|
||||
this.languageCache.delete(language);
|
||||
this.moduleCache.delete(language);
|
||||
|
||||
// 重新加载通用语言包
|
||||
await this.loadModuleLanguagePack('common', 'api', language);
|
||||
await this.loadModuleLanguagePack('common', 'dict', language);
|
||||
await this.loadModuleLanguagePack('common', 'validate', language);
|
||||
|
||||
this.logger.log(`重新加载语言包完成: ${language}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`重新加载语言包失败: ${language}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载所有语言包
|
||||
*/
|
||||
async reloadAllLanguagePacks(): Promise<void> {
|
||||
try {
|
||||
this.languageCache.clear();
|
||||
this.moduleCache.clear();
|
||||
await this.initializeLanguagePacks();
|
||||
this.logger.log('所有语言包重新加载完成');
|
||||
} catch (error) {
|
||||
this.logger.error('重新加载所有语言包失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API消息
|
||||
* @param key 消息键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param language 语言
|
||||
*/
|
||||
async getApiMessage(key: string, args?: any, module: string = 'common', language?: string): Promise<string> {
|
||||
const currentLanguage = language || this.getCurrentLanguage();
|
||||
return this.getMessage(key, args, module, 'api', currentLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
* @param key 字典键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param language 语言
|
||||
*/
|
||||
async getDictData(key: string, args?: any, module: string = 'common', language?: string): Promise<string> {
|
||||
const currentLanguage = language || this.getCurrentLanguage();
|
||||
return this.getMessage(key, args, module, 'dict', currentLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证器消息
|
||||
* @param key 验证器键
|
||||
* @param args 参数
|
||||
* @param module 模块名
|
||||
* @param language 语言
|
||||
*/
|
||||
async getValidateMessage(key: string, args?: any, module: string = 'common', language?: string): Promise<string> {
|
||||
const currentLanguage = language || this.getCurrentLanguage();
|
||||
return this.getMessage(key, args, module, 'validate', currentLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取消息
|
||||
* @param keys 消息键数组
|
||||
* @param module 模块名
|
||||
* @param type 类型
|
||||
* @param language 语言
|
||||
*/
|
||||
async getBatchMessages(keys: string[], module: string = 'common', type: string = 'api', language?: string): Promise<Record<string, string>> {
|
||||
const results: Record<string, string> = {};
|
||||
const currentLanguage = language || this.getCurrentLanguage();
|
||||
|
||||
for (const key of keys) {
|
||||
results[key] = await this.getMessage(key, undefined, module, type, currentLanguage);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
15
src/common/libraries/dayjs/dayjs.module.ts
Normal file
15
src/common/libraries/dayjs/dayjs.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { DayjsService } from './dayjs.service';
|
||||
|
||||
/**
|
||||
* Day.js 日期处理库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: DateUtils
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [DayjsService],
|
||||
exports: [DayjsService],
|
||||
})
|
||||
export class DayjsModule {}
|
||||
346
src/common/libraries/dayjs/dayjs.service.ts
Normal file
346
src/common/libraries/dayjs/dayjs.service.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import isBetween from 'dayjs/plugin/isBetween';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
|
||||
import dayOfYear from 'dayjs/plugin/dayOfYear';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
|
||||
/**
|
||||
* Day.js 日期处理服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: DateUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class DayjsService {
|
||||
constructor() {
|
||||
// 初始化插件
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(quarterOfYear);
|
||||
dayjs.extend(dayOfYear);
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 dayjs 实例
|
||||
*/
|
||||
getDayjs() {
|
||||
return dayjs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
format(
|
||||
date: Date | string | number,
|
||||
format: string = 'YYYY-MM-DD HH:mm:ss',
|
||||
): string {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串
|
||||
*/
|
||||
parse(dateStr: string, format?: string): dayjs.Dayjs | null {
|
||||
if (!dateStr) return null;
|
||||
|
||||
try {
|
||||
const parsed = format ? dayjs(dateStr, format) : dayjs(dateStr);
|
||||
return parsed.isValid() ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间
|
||||
*/
|
||||
now(): dayjs.Dayjs {
|
||||
return dayjs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳
|
||||
*/
|
||||
timestamp(): number {
|
||||
return dayjs().valueOf();
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间戳转日期
|
||||
*/
|
||||
fromTimestamp(timestamp: number): dayjs.Dayjs {
|
||||
return dayjs(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期转时间戳
|
||||
*/
|
||||
toTimestamp(date: Date | string | number): number {
|
||||
return dayjs(date).valueOf();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加时间
|
||||
*/
|
||||
add(
|
||||
date: Date | string | number,
|
||||
amount: number,
|
||||
unit: dayjs.ManipulateType,
|
||||
): dayjs.Dayjs {
|
||||
return dayjs(date).add(amount, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 减去时间
|
||||
*/
|
||||
subtract(
|
||||
date: Date | string | number,
|
||||
amount: number,
|
||||
unit: dayjs.ManipulateType,
|
||||
): dayjs.Dayjs {
|
||||
return dayjs(date).subtract(amount, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算时间差
|
||||
*/
|
||||
diff(
|
||||
date1: Date | string | number,
|
||||
date2: Date | string | number,
|
||||
unit: dayjs.QUnitType = 'millisecond',
|
||||
): number {
|
||||
return dayjs(date1).diff(dayjs(date2), unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取开始时间
|
||||
*/
|
||||
startOf(date: Date | string | number, unit: dayjs.OpUnitType): dayjs.Dayjs {
|
||||
return dayjs(date).startOf(unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取结束时间
|
||||
*/
|
||||
endOf(date: Date | string | number, unit: dayjs.OpUnitType): dayjs.Dayjs {
|
||||
return dayjs(date).endOf(unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为今天
|
||||
*/
|
||||
isToday(date: Date | string | number): boolean {
|
||||
return dayjs(date).isSame(dayjs(), 'day');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为昨天
|
||||
*/
|
||||
isYesterday(date: Date | string | number): boolean {
|
||||
return dayjs(date).isSame(dayjs().subtract(1, 'day'), 'day');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为明天
|
||||
*/
|
||||
isTomorrow(date: Date | string | number): boolean {
|
||||
return dayjs(date).isSame(dayjs().add(1, 'day'), 'day');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为同一天
|
||||
*/
|
||||
isSameDay(
|
||||
date1: Date | string | number,
|
||||
date2: Date | string | number,
|
||||
): boolean {
|
||||
return dayjs(date1).isSame(dayjs(date2), 'day');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否在范围内
|
||||
*/
|
||||
isBetween(
|
||||
date: Date | string | number,
|
||||
start: Date | string | number,
|
||||
end: Date | string | number,
|
||||
unit?: dayjs.OpUnitType,
|
||||
): boolean {
|
||||
return dayjs(date).isBetween(dayjs(start), dayjs(end), unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对时间描述
|
||||
*/
|
||||
fromNow(date: Date | string | number): string {
|
||||
return dayjs(date).fromNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对时间描述(中文)
|
||||
*/
|
||||
fromNowZh(date: Date | string | number): string {
|
||||
const now = dayjs();
|
||||
const target = dayjs(date);
|
||||
const diffMs = now.diff(target);
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffSeconds < 60) {
|
||||
return '刚刚';
|
||||
} else if (diffMinutes < 60) {
|
||||
return `${diffMinutes}分钟前`;
|
||||
} else if (diffHours < 24) {
|
||||
return `${diffHours}小时前`;
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`;
|
||||
} else {
|
||||
return target.format('YYYY-MM-DD');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置时区
|
||||
*/
|
||||
setTimezone(date: Date | string | number, timezone: string): dayjs.Dayjs {
|
||||
return dayjs(date).tz(timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 UTC
|
||||
*/
|
||||
toUTC(date: Date | string | number): dayjs.Dayjs {
|
||||
return dayjs(date).utc();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为本地时间
|
||||
*/
|
||||
toLocal(date: Date | string | number): dayjs.Dayjs {
|
||||
return dayjs(date).local();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取持续时间
|
||||
*/
|
||||
duration(amount: number, unit: any): any {
|
||||
return dayjs.duration(amount, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断日期是否有效
|
||||
*/
|
||||
isValid(date: any): boolean {
|
||||
return dayjs(date).isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取月份天数
|
||||
*/
|
||||
daysInMonth(date: Date | string | number): number {
|
||||
return dayjs(date).daysInMonth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份
|
||||
*/
|
||||
year(date: Date | string | number): number {
|
||||
return dayjs(date).year();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取月份(0-11)
|
||||
*/
|
||||
month(date: Date | string | number): number {
|
||||
return dayjs(date).month();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日期
|
||||
*/
|
||||
date(date: Date | string | number): number {
|
||||
return dayjs(date).date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小时
|
||||
*/
|
||||
hour(date: Date | string | number): number {
|
||||
return dayjs(date).hour();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分钟
|
||||
*/
|
||||
minute(date: Date | string | number): number {
|
||||
return dayjs(date).minute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒
|
||||
*/
|
||||
second(date: Date | string | number): number {
|
||||
return dayjs(date).second();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取毫秒
|
||||
*/
|
||||
millisecond(date: Date | string | number): number {
|
||||
return dayjs(date).millisecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取星期几
|
||||
*/
|
||||
day(date: Date | string | number): number {
|
||||
return dayjs(date).day();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取季度
|
||||
*/
|
||||
quarter(date: Date | string | number): number {
|
||||
return dayjs(date).quarter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份中的第几天
|
||||
*/
|
||||
dayOfYear(date: Date | string | number): number {
|
||||
return dayjs(date).dayOfYear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份中的第几周
|
||||
*/
|
||||
week(date: Date | string | number): number {
|
||||
return dayjs(date).week();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份中的第几周(ISO)
|
||||
*/
|
||||
isoWeek(date: Date | string | number): number {
|
||||
return dayjs(date).isoWeek();
|
||||
}
|
||||
}
|
||||
10
src/common/libraries/index.ts
Normal file
10
src/common/libraries/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './libraries.module';
|
||||
export * from './dayjs/dayjs.service';
|
||||
export * from './lodash/lodash.service';
|
||||
export * from './prometheus/prometheus.service';
|
||||
export * from './redis/redis.service';
|
||||
export * from './sentry/sentry.service';
|
||||
export * from './sharp/sharp.service';
|
||||
export * from './uuid/uuid.service';
|
||||
export * from './validator/validator.service';
|
||||
export * from './winston/winston.service';
|
||||
43
src/common/libraries/libraries.module.ts
Normal file
43
src/common/libraries/libraries.module.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DayjsModule } from './dayjs/dayjs.module';
|
||||
import { LodashModule } from './lodash/lodash.module';
|
||||
import { PrometheusModule } from './prometheus/prometheus.module';
|
||||
import { RedisModule } from './redis/redis.module';
|
||||
import { SentryModule } from './sentry/sentry.module';
|
||||
import { SharpModule } from './sharp/sharp.module';
|
||||
import { UuidModule } from './uuid/uuid.module';
|
||||
import { ValidatorModule } from './validator/validator.module';
|
||||
import { WinstonModule } from './winston/winston.module';
|
||||
|
||||
/**
|
||||
* 第三方工具库模块 - Common层
|
||||
* 基于 NestJS 实现
|
||||
* 对应 Java: 工具库封装
|
||||
*
|
||||
* 包含所有第三方工具库的封装,作为框架基础能力
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
DayjsModule,
|
||||
LodashModule,
|
||||
PrometheusModule,
|
||||
RedisModule,
|
||||
SentryModule,
|
||||
SharpModule,
|
||||
UuidModule,
|
||||
ValidatorModule,
|
||||
WinstonModule,
|
||||
],
|
||||
exports: [
|
||||
DayjsModule,
|
||||
LodashModule,
|
||||
PrometheusModule,
|
||||
RedisModule,
|
||||
SentryModule,
|
||||
SharpModule,
|
||||
UuidModule,
|
||||
ValidatorModule,
|
||||
WinstonModule,
|
||||
],
|
||||
})
|
||||
export class LibrariesModule {}
|
||||
15
src/common/libraries/lodash/lodash.module.ts
Normal file
15
src/common/libraries/lodash/lodash.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { LodashService } from './lodash.service';
|
||||
|
||||
/**
|
||||
* Lodash 工具函数库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: StringUtils, ObjectUtils, ArrayUtils
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [LodashService],
|
||||
exports: [LodashService],
|
||||
})
|
||||
export class LodashModule {}
|
||||
665
src/common/libraries/lodash/lodash.service.ts
Normal file
665
src/common/libraries/lodash/lodash.service.ts
Normal file
@@ -0,0 +1,665 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
/**
|
||||
* Lodash 工具函数服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: StringUtils, ObjectUtils, ArrayUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class LodashService {
|
||||
/**
|
||||
* 获取 lodash 实例
|
||||
*/
|
||||
getLodash(): any {
|
||||
return _;
|
||||
}
|
||||
|
||||
// ==================== 字符串工具 ====================
|
||||
|
||||
/**
|
||||
* 判断字符串是否为空
|
||||
*/
|
||||
isEmpty(value: any): boolean {
|
||||
return _.isEmpty(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否不为空
|
||||
*/
|
||||
isNotEmpty(value: any): boolean {
|
||||
return !_.isEmpty(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
*/
|
||||
capitalize(str: string): string {
|
||||
return _.capitalize(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转下划线
|
||||
*/
|
||||
snakeCase(str: string): string {
|
||||
return _.snakeCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
*/
|
||||
camelCase(str: string): string {
|
||||
return _.camelCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为大写
|
||||
*/
|
||||
upperCase(str: string): string {
|
||||
return _.upperCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为小写
|
||||
*/
|
||||
lowerCase(str: string): string {
|
||||
return _.lowerCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为标题格式
|
||||
*/
|
||||
startCase(str: string): string {
|
||||
return _.startCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取字符串
|
||||
*/
|
||||
truncate(str: string, options?: _.TruncateOptions): string {
|
||||
return _.truncate(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串两端空白
|
||||
*/
|
||||
trim(str: string, chars?: string): string {
|
||||
return _.trim(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串左端空白
|
||||
*/
|
||||
trimStart(str: string, chars?: string): string {
|
||||
return _.trimStart(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串右端空白
|
||||
*/
|
||||
trimEnd(str: string, chars?: string): string {
|
||||
return _.trimEnd(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重复字符串
|
||||
*/
|
||||
repeat(str: string, n: number): string {
|
||||
return _.repeat(str, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
randomString(
|
||||
length: number = 8,
|
||||
chars: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
||||
): string {
|
||||
return _.sampleSize(chars, length).join('');
|
||||
}
|
||||
|
||||
// ==================== 对象工具 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为对象
|
||||
*/
|
||||
isObject(value: any): boolean {
|
||||
return _.isObject(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为数组
|
||||
*/
|
||||
isArray(value: any): boolean {
|
||||
return _.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为函数
|
||||
*/
|
||||
isFunction(value: any): boolean {
|
||||
return _.isFunction(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为数字
|
||||
*/
|
||||
isNumber(value: any): boolean {
|
||||
return _.isNumber(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为字符串
|
||||
*/
|
||||
isString(value: any): boolean {
|
||||
return _.isString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为布尔值
|
||||
*/
|
||||
isBoolean(value: any): boolean {
|
||||
return _.isBoolean(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为日期
|
||||
*/
|
||||
isDate(value: any): boolean {
|
||||
return _.isDate(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 null
|
||||
*/
|
||||
isNull(value: any): boolean {
|
||||
return _.isNull(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 undefined
|
||||
*/
|
||||
isUndefined(value: any): boolean {
|
||||
return _.isUndefined(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 null 或 undefined
|
||||
*/
|
||||
isNil(value: any): boolean {
|
||||
return _.isNil(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并对象
|
||||
*/
|
||||
merge<T extends object>(target: T, ...sources: Partial<T>[]): T {
|
||||
return _.merge(target, ...sources);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从对象中提取指定属性
|
||||
*/
|
||||
pick<T extends object, K extends keyof T>(
|
||||
object: T,
|
||||
...paths: K[]
|
||||
): Pick<T, K> {
|
||||
return _.pick(object, ...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从对象中排除指定属性
|
||||
*/
|
||||
omit<T extends object, K extends keyof T>(
|
||||
object: T,
|
||||
...paths: K[]
|
||||
): Omit<T, K> {
|
||||
return _.omit(object, ...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象属性值
|
||||
*/
|
||||
get<T = any>(object: any, path: string, defaultValue?: T): T {
|
||||
return _.get(object, path, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置对象属性值
|
||||
*/
|
||||
set<T = any>(object: any, path: string, value: T): any {
|
||||
return _.set(object, path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象是否有指定属性
|
||||
*/
|
||||
has(object: any, path: string): boolean {
|
||||
return _.has(object, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除对象属性
|
||||
*/
|
||||
unset(object: any, path: string): boolean {
|
||||
return _.unset(object, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有键
|
||||
*/
|
||||
keys(object: any): string[] {
|
||||
return _.keys(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有值
|
||||
*/
|
||||
values(object: any): any[] {
|
||||
return _.values(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有键值对
|
||||
*/
|
||||
entries(object: any): [string, any][] {
|
||||
return _.entries(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将键值对数组转换为对象
|
||||
*/
|
||||
fromPairs(pairs: [string, any][]): object {
|
||||
return _.fromPairs(pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为键值对数组
|
||||
*/
|
||||
toPairs(object: any): [string, any][] {
|
||||
return _.toPairs(object);
|
||||
}
|
||||
|
||||
// ==================== 数组工具 ====================
|
||||
|
||||
/**
|
||||
* 数组去重
|
||||
*/
|
||||
uniq<T>(array: T[]): T[] {
|
||||
return _.uniq(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组去重(根据指定属性)
|
||||
*/
|
||||
uniqBy<T>(array: T[], iteratee: string | ((item: T) => any)): T[] {
|
||||
return _.uniqBy(array, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组去重(根据指定函数)
|
||||
*/
|
||||
uniqWith<T>(array: T[], comparator: (a: T, b: T) => boolean): T[] {
|
||||
return _.uniqWith(array, comparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组分组
|
||||
*/
|
||||
groupBy<T>(
|
||||
array: T[],
|
||||
iteratee: string | ((item: T) => any),
|
||||
): Record<string, T[]> {
|
||||
return _.groupBy(array, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组排序
|
||||
*/
|
||||
sortBy<T>(array: T[], ...iteratees: (string | ((item: T) => any))[]): T[] {
|
||||
return _.sortBy(array, ...iteratees);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组过滤
|
||||
*/
|
||||
filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
|
||||
return _.filter(array, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组映射
|
||||
*/
|
||||
map<T, U>(array: T[], iteratee: (item: T, index: number) => U): U[] {
|
||||
return _.map(array, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组查找
|
||||
*/
|
||||
find<T>(array: T[], predicate: (item: T) => boolean): T | undefined {
|
||||
return _.find(array, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组查找索引
|
||||
*/
|
||||
findIndex<T>(array: T[], predicate: (item: T) => boolean): number {
|
||||
return _.findIndex(array, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组包含
|
||||
*/
|
||||
includes<T>(array: T[], value: T): boolean {
|
||||
return _.includes(array, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组切片
|
||||
*/
|
||||
slice<T>(array: T[], start?: number, end?: number): T[] {
|
||||
return _.slice(array, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组分块
|
||||
*/
|
||||
chunk<T>(array: T[], size: number): T[][] {
|
||||
return _.chunk(array, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组扁平化
|
||||
*/
|
||||
flatten<T>(array: T[][]): T[] {
|
||||
return _.flatten(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组深度扁平化
|
||||
*/
|
||||
flattenDeep<T>(array: any[]): T[] {
|
||||
return _.flattenDeep(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组压缩
|
||||
*/
|
||||
compact<T>(array: T[]): T[] {
|
||||
return _.compact(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组连接
|
||||
*/
|
||||
concat<T>(array: T[], ...values: any[]): T[] {
|
||||
return _.concat(array, ...values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组差集
|
||||
*/
|
||||
difference<T>(array: T[], ...values: T[][]): T[] {
|
||||
return _.difference(array, ...values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组交集
|
||||
*/
|
||||
intersection<T>(...arrays: T[][]): T[] {
|
||||
return _.intersection(...arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组并集
|
||||
*/
|
||||
union<T>(...arrays: T[][]): T[] {
|
||||
return _.union(...arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组求和
|
||||
*/
|
||||
sum(array: number[]): number {
|
||||
return _.sum(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组平均值
|
||||
*/
|
||||
mean(array: number[]): number {
|
||||
return _.mean(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组最大值
|
||||
*/
|
||||
max<T>(array: T[]): T | undefined {
|
||||
return _.max(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组最小值
|
||||
*/
|
||||
min<T>(array: T[]): T | undefined {
|
||||
return _.min(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组随机元素
|
||||
*/
|
||||
sample<T>(array: T[]): T | undefined {
|
||||
return _.sample(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组随机元素(多个)
|
||||
*/
|
||||
sampleSize<T>(array: T[], n: number): T[] {
|
||||
return _.sampleSize(array, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组洗牌
|
||||
*/
|
||||
shuffle<T>(array: T[]): T[] {
|
||||
return _.shuffle(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组取前N个
|
||||
*/
|
||||
take<T>(array: T[], n: number): T[] {
|
||||
return _.take(array, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组取后N个
|
||||
*/
|
||||
takeRight<T>(array: T[], n: number): T[] {
|
||||
return _.takeRight(array, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组跳过前N个
|
||||
*/
|
||||
drop<T>(array: T[], n: number): T[] {
|
||||
return _.drop(array, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组跳过后N个
|
||||
*/
|
||||
dropRight<T>(array: T[], n: number): T[] {
|
||||
return _.dropRight(array, n);
|
||||
}
|
||||
|
||||
// ==================== 函数工具 ====================
|
||||
|
||||
/**
|
||||
* 函数防抖
|
||||
*/
|
||||
debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number,
|
||||
options?: _.DebounceSettings,
|
||||
): any {
|
||||
return _.debounce(func, wait, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数节流
|
||||
*/
|
||||
throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number,
|
||||
options?: _.ThrottleSettings,
|
||||
): any {
|
||||
return _.throttle(func, wait, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数柯里化
|
||||
*/
|
||||
curry<T extends (...args: any[]) => any>(func: T): any {
|
||||
return _.curry(func);
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数记忆化
|
||||
*/
|
||||
memoize<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
resolver?: (...args: any[]) => string,
|
||||
): T {
|
||||
return _.memoize(func, resolver);
|
||||
}
|
||||
|
||||
// ==================== 数字工具 ====================
|
||||
|
||||
/**
|
||||
* 数字范围
|
||||
*/
|
||||
range(start: number, end?: number, step?: number): number[] {
|
||||
return _.range(start, end, step);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字随机
|
||||
*/
|
||||
random(lower: number, upper?: number, floating?: boolean): number {
|
||||
if (upper === undefined) {
|
||||
return _.random(lower);
|
||||
}
|
||||
return _.random(lower, upper, floating);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字四舍五入
|
||||
*/
|
||||
round(number: number, precision?: number): number {
|
||||
return _.round(number, precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字向上取整
|
||||
*/
|
||||
ceil(number: number, precision?: number): number {
|
||||
return _.ceil(number, precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字向下取整
|
||||
*/
|
||||
floor(number: number, precision?: number): number {
|
||||
return _.floor(number, precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字截断
|
||||
*/
|
||||
trunc(number: number, precision?: number): number {
|
||||
return (
|
||||
Math.trunc(number * Math.pow(10, precision || 0)) /
|
||||
Math.pow(10, precision || 0)
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 集合工具 ====================
|
||||
|
||||
/**
|
||||
* 集合大小
|
||||
*/
|
||||
size(collection: any): number {
|
||||
return _.size(collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合遍历
|
||||
*/
|
||||
forEach<T>(
|
||||
collection: T[],
|
||||
iteratee: (value: T, index: number, collection: T[]) => void,
|
||||
): T[] {
|
||||
return _.forEach(collection, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合映射
|
||||
*/
|
||||
mapValues<T, U>(
|
||||
object: Record<string, T>,
|
||||
iteratee: (value: T, key: string) => U,
|
||||
): Record<string, U> {
|
||||
return _.mapValues(object, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键映射
|
||||
*/
|
||||
mapKeys<T>(
|
||||
object: Record<string, T>,
|
||||
iteratee: (value: T, key: string) => string,
|
||||
): Record<string, T> {
|
||||
return _.mapKeys(object, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键过滤
|
||||
*/
|
||||
pickBy<T>(
|
||||
object: Record<string, T>,
|
||||
predicate: (value: T, key: string) => boolean,
|
||||
): Record<string, T> {
|
||||
return _.pickBy(object, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键排除
|
||||
*/
|
||||
omitBy<T>(
|
||||
object: Record<string, T>,
|
||||
predicate: (value: T, key: string) => boolean,
|
||||
): Record<string, T> {
|
||||
return _.omitBy(object, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键反转
|
||||
*/
|
||||
invert(object: Record<string, any>): Record<string, string> {
|
||||
return _.invert(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键反转(保持值)
|
||||
*/
|
||||
invertBy<T>(
|
||||
object: Record<string, T>,
|
||||
iteratee?: (value: T) => string,
|
||||
): Record<string, string[]> {
|
||||
return _.invertBy(object, iteratee);
|
||||
}
|
||||
}
|
||||
26
src/common/libraries/prometheus/prometheus.module.ts
Normal file
26
src/common/libraries/prometheus/prometheus.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { PrometheusService } from './prometheus.service';
|
||||
|
||||
/**
|
||||
* Prometheus 客户端模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: Prometheus 监控
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PrometheusService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const prometheusConfig = configService.get('prometheus');
|
||||
return new PrometheusService(prometheusConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [PrometheusService],
|
||||
})
|
||||
export class PrometheusModule {}
|
||||
428
src/common/libraries/prometheus/prometheus.service.ts
Normal file
428
src/common/libraries/prometheus/prometheus.service.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import {
|
||||
register,
|
||||
Counter,
|
||||
Histogram,
|
||||
Summary,
|
||||
Gauge,
|
||||
collectDefaultMetrics,
|
||||
Registry,
|
||||
Metric,
|
||||
} from 'prom-client';
|
||||
import type {
|
||||
MonitoringInterface,
|
||||
Timer,
|
||||
MonitoringConfig,
|
||||
} from '../../monitoring/monitoring.interface';
|
||||
|
||||
/**
|
||||
* Prometheus 服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: Prometheus 监控
|
||||
*/
|
||||
@Injectable()
|
||||
export class PrometheusService implements MonitoringInterface, OnModuleInit {
|
||||
private readonly logger = new Logger(PrometheusService.name);
|
||||
private registry: Registry;
|
||||
private metrics = new Map<string, Metric>();
|
||||
private timers = new Map<string, number>();
|
||||
|
||||
constructor(
|
||||
private readonly config: MonitoringConfig = {
|
||||
enabled: false,
|
||||
port: 9090,
|
||||
path: '/metrics',
|
||||
prefix: 'wwjcloud',
|
||||
defaultLabels: {},
|
||||
collectDefaultMetrics: false,
|
||||
},
|
||||
) {
|
||||
this.registry = new Registry();
|
||||
this.initializeRegistry();
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
if (this.config.collectDefaultMetrics) {
|
||||
collectDefaultMetrics({ register: this.registry });
|
||||
this.logger.log('Default metrics collection enabled');
|
||||
}
|
||||
}
|
||||
|
||||
private initializeRegistry() {
|
||||
// 设置默认标签
|
||||
if (this.config.defaultLabels) {
|
||||
this.registry.setDefaultLabels(this.config.defaultLabels);
|
||||
}
|
||||
|
||||
// 设置前缀
|
||||
if (this.config.prefix) {
|
||||
this.registry.setDefaultLabels({
|
||||
// ...this.registry.getDefaultLabels(),
|
||||
prefix: this.config.prefix,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录计数器指标
|
||||
*/
|
||||
counter(
|
||||
name: string,
|
||||
value: number = 1,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
let counter = this.metrics.get(metricName) as Counter<string>;
|
||||
|
||||
if (!counter) {
|
||||
counter = new Counter({
|
||||
name: metricName,
|
||||
help: `Counter metric for ${name}`,
|
||||
labelNames: Object.keys(labels || {}),
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, counter);
|
||||
}
|
||||
|
||||
counter.inc(labels || {}, value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record counter: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录直方图指标
|
||||
*/
|
||||
histogram(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
let histogram = this.metrics.get(metricName) as Histogram<string>;
|
||||
|
||||
if (!histogram) {
|
||||
histogram = new Histogram({
|
||||
name: metricName,
|
||||
help: `Histogram metric for ${name}`,
|
||||
labelNames: Object.keys(labels || {}),
|
||||
buckets: [0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, histogram);
|
||||
}
|
||||
|
||||
histogram.observe(labels || {}, value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record histogram: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录摘要指标
|
||||
*/
|
||||
summary(name: string, value: number, labels?: Record<string, string>): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
let summary = this.metrics.get(metricName) as Summary<string>;
|
||||
|
||||
if (!summary) {
|
||||
summary = new Summary({
|
||||
name: metricName,
|
||||
help: `Summary metric for ${name}`,
|
||||
labelNames: Object.keys(labels || {}),
|
||||
percentiles: [0.5, 0.9, 0.95, 0.99],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, summary);
|
||||
}
|
||||
|
||||
summary.observe(labels || {}, value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record summary: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录仪表盘指标
|
||||
*/
|
||||
gauge(name: string, value: number, labels?: Record<string, string>): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
let gauge = this.metrics.get(metricName) as Gauge<string>;
|
||||
|
||||
if (!gauge) {
|
||||
gauge = new Gauge({
|
||||
name: metricName,
|
||||
help: `Gauge metric for ${name}`,
|
||||
labelNames: Object.keys(labels || {}),
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, gauge);
|
||||
}
|
||||
|
||||
gauge.set(labels || {}, value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record gauge: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始计时
|
||||
*/
|
||||
startTimer(name: string, labels?: Record<string, string>): Timer {
|
||||
const timerId = `${name}_${JSON.stringify(labels || {})}`;
|
||||
this.timers.set(timerId, Date.now());
|
||||
|
||||
return {
|
||||
end: (endLabels?: Record<string, string>) => {
|
||||
const startTime = this.timers.get(timerId);
|
||||
if (startTime) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.timers.delete(timerId);
|
||||
this.histogram(name, duration / 1000, endLabels || labels);
|
||||
return duration;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录执行时间
|
||||
*/
|
||||
async time<T>(
|
||||
name: string,
|
||||
fn: () => T | Promise<T>,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<T> {
|
||||
const timer = this.startTimer(name, labels);
|
||||
try {
|
||||
const result = await fn();
|
||||
return result;
|
||||
} finally {
|
||||
timer.end(labels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标值
|
||||
*/
|
||||
async getMetric(
|
||||
name: string,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
|
||||
if (metric) {
|
||||
const metricData = metric.get();
|
||||
const data = await metricData;
|
||||
if (data && data.values) {
|
||||
const value = data.values.find(
|
||||
(v) =>
|
||||
!labels ||
|
||||
Object.keys(labels).every((key) => v.labels[key] === labels[key]),
|
||||
);
|
||||
return value ? value.value : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get metric: ${name}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有指标
|
||||
*/
|
||||
async getMetrics(): Promise<string> {
|
||||
try {
|
||||
return await this.registry.metrics();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get metrics', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指标
|
||||
*/
|
||||
reset(name?: string): void {
|
||||
try {
|
||||
if (name) {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
if (metric) {
|
||||
metric.reset();
|
||||
}
|
||||
} else {
|
||||
this.registry.clear();
|
||||
this.metrics.clear();
|
||||
this.timers.clear();
|
||||
this.initializeRegistry();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to reset metrics: ${name || 'all'}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册表
|
||||
*/
|
||||
getRegistry(): Registry {
|
||||
return this.registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标列表
|
||||
*/
|
||||
getMetricList(): string[] {
|
||||
return Array.from(this.metrics.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指标是否存在
|
||||
*/
|
||||
hasMetric(name: string): boolean {
|
||||
const metricName = this.getMetricName(name);
|
||||
return this.metrics.has(metricName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指标
|
||||
*/
|
||||
removeMetric(name: string): boolean {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
if (metric) {
|
||||
this.registry.removeSingleMetric(metricName);
|
||||
this.metrics.delete(metricName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标数据
|
||||
*/
|
||||
getMetricData(name: string): any {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
return metric ? metric.get() : null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get metric data: ${name}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置指标帮助文本
|
||||
*/
|
||||
setMetricHelp(name: string, help: string): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
if (metric && 'help' in metric) {
|
||||
(metric as any).help = help;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set metric help: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标名称
|
||||
*/
|
||||
private getMetricName(name: string): string {
|
||||
const prefix = this.config.prefix || 'app';
|
||||
return `${prefix}_${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义指标
|
||||
*/
|
||||
createCounter(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames: string[] = [],
|
||||
): Counter<string> {
|
||||
const metricName = this.getMetricName(name);
|
||||
const counter = new Counter({
|
||||
name: metricName,
|
||||
help,
|
||||
labelNames,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, counter);
|
||||
return counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义直方图
|
||||
*/
|
||||
createHistogram(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames: string[] = [],
|
||||
buckets: number[] = [0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600],
|
||||
): Histogram<string> {
|
||||
const metricName = this.getMetricName(name);
|
||||
const histogram = new Histogram({
|
||||
name: metricName,
|
||||
help,
|
||||
labelNames,
|
||||
buckets,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, histogram);
|
||||
return histogram;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义摘要
|
||||
*/
|
||||
createSummary(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames: string[] = [],
|
||||
percentiles: number[] = [0.5, 0.9, 0.95, 0.99],
|
||||
): Summary<string> {
|
||||
const metricName = this.getMetricName(name);
|
||||
const summary = new Summary({
|
||||
name: metricName,
|
||||
help,
|
||||
labelNames,
|
||||
percentiles,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义仪表盘
|
||||
*/
|
||||
createGauge(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames: string[] = [],
|
||||
): Gauge<string> {
|
||||
const metricName = this.getMetricName(name);
|
||||
const gauge = new Gauge({
|
||||
name: metricName,
|
||||
help,
|
||||
labelNames,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, gauge);
|
||||
return gauge;
|
||||
}
|
||||
}
|
||||
26
src/common/libraries/redis/redis.module.ts
Normal file
26
src/common/libraries/redis/redis.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
/**
|
||||
* Redis 客户端模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: RedisTemplate
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: RedisService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const redisConfig = configService.get('redis');
|
||||
return new RedisService(redisConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
||||
488
src/common/libraries/redis/redis.service.ts
Normal file
488
src/common/libraries/redis/redis.service.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
import type { CacheInterface, CacheConfig } from '../../cache/cache.interface';
|
||||
import { CacheStats } from '../../cache/cache.interface';
|
||||
|
||||
/**
|
||||
* Redis 服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: RedisUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class RedisService implements CacheInterface, OnModuleDestroy {
|
||||
private readonly logger = new Logger(RedisService.name);
|
||||
private client: RedisClientType;
|
||||
private cacheStats: CacheStats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
keys: 0,
|
||||
memory: 0,
|
||||
uptime: 0,
|
||||
};
|
||||
private startTime: number;
|
||||
|
||||
constructor(private readonly config: CacheConfig) {
|
||||
this.startTime = Date.now();
|
||||
this.initializeClient();
|
||||
}
|
||||
|
||||
private async initializeClient() {
|
||||
try {
|
||||
this.client = createClient({
|
||||
socket: {
|
||||
host: this.config.host,
|
||||
port: this.config.port,
|
||||
},
|
||||
password: this.config.password,
|
||||
database: this.config.db || 0,
|
||||
});
|
||||
|
||||
this.client.on('error', (err) => {
|
||||
this.logger.error('Redis Client Error', err);
|
||||
});
|
||||
|
||||
this.client.on('connect', () => {
|
||||
this.logger.log('Redis Client Connected');
|
||||
});
|
||||
|
||||
this.client.on('ready', () => {
|
||||
this.logger.log('Redis Client Ready');
|
||||
});
|
||||
|
||||
this.client.on('end', () => {
|
||||
this.logger.log('Redis Client Disconnected');
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize Redis client', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.client) {
|
||||
await this.client.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const value = await this.client.get(prefixedKey);
|
||||
|
||||
if (value === null) {
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.cacheStats.hits++;
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get cache key: ${key}`, error);
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const serializedValue = JSON.stringify(value);
|
||||
|
||||
if (ttl) {
|
||||
await this.client.setEx(prefixedKey, ttl, serializedValue);
|
||||
} else {
|
||||
await this.client.set(prefixedKey, serializedValue);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set cache key: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
async del(key: string): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
await this.client.del(prefixedKey);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to delete cache key: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除缓存
|
||||
*/
|
||||
async delMany(keys: string[]): Promise<void> {
|
||||
try {
|
||||
const prefixedKeys = keys.map((key) => this.getPrefixedKey(key));
|
||||
await this.client.del(prefixedKeys);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to delete cache keys: ${keys.join(', ')}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在
|
||||
*/
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const result = await this.client.exists(prefixedKey);
|
||||
return result === 1;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to check cache key: ${key}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
*/
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
await this.client.expire(prefixedKey, ttl);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set expire for cache key: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期时间
|
||||
*/
|
||||
async ttl(key: string): Promise<number> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
return await this.client.ttl(prefixedKey);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get ttl for cache key: ${key}`, error);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有键
|
||||
*/
|
||||
async keys(pattern?: string): Promise<string[]> {
|
||||
try {
|
||||
const searchPattern = pattern
|
||||
? this.getPrefixedKey(pattern)
|
||||
: `${this.config.keyPrefix || 'cache'}:*`;
|
||||
const keys = await this.client.keys(searchPattern);
|
||||
return keys.map((key) => this.removePrefix(key));
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to get cache keys with pattern: ${pattern}`,
|
||||
error,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
async flush(): Promise<void> {
|
||||
try {
|
||||
await this.client.flushDb();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to flush cache', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
async stats(): Promise<CacheStats> {
|
||||
try {
|
||||
const info = await this.client.info('memory');
|
||||
const memoryMatch = info.match(/used_memory:(\d+)/);
|
||||
const memory = memoryMatch ? parseInt(memoryMatch[1]) : 0;
|
||||
|
||||
const keyCount = await this.client.dbSize();
|
||||
|
||||
return {
|
||||
...this.cacheStats,
|
||||
keys: keyCount,
|
||||
memory,
|
||||
uptime: Date.now() - this.startTime,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get cache stats', error);
|
||||
return this.cacheStats;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取
|
||||
*/
|
||||
async mget<T = any>(keys: string[]): Promise<(T | null)[]> {
|
||||
try {
|
||||
const prefixedKeys = keys.map((key) => this.getPrefixedKey(key));
|
||||
const values = await this.client.mGet(prefixedKeys);
|
||||
|
||||
return values.map((value) => {
|
||||
if (value === null) {
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
this.cacheStats.hits++;
|
||||
return JSON.parse(value);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to mget cache keys: ${keys.join(', ')}`, error);
|
||||
return keys.map(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置
|
||||
*/
|
||||
async mset(
|
||||
keyValuePairs: Array<{ key: string; value: any; ttl?: number }>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const pipeline = this.client.multi();
|
||||
|
||||
for (const { key, value, ttl } of keyValuePairs) {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const serializedValue = JSON.stringify(value);
|
||||
|
||||
if (ttl) {
|
||||
pipeline.setEx(prefixedKey, ttl, serializedValue);
|
||||
} else {
|
||||
pipeline.set(prefixedKey, serializedValue);
|
||||
}
|
||||
}
|
||||
|
||||
await pipeline.exec();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to mset cache', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
async incr(key: string, increment: number = 1): Promise<number> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
return await this.client.incrBy(prefixedKey, increment);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to incr cache key: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减
|
||||
*/
|
||||
async decr(key: string, decrement: number = 1): Promise<number> {
|
||||
return this.incr(key, -decrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置哈希字段
|
||||
*/
|
||||
async hset(key: string, field: string, value: any): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const serializedValue = JSON.stringify(value);
|
||||
await this.client.hSet(prefixedKey, field, serializedValue);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to hset cache key: ${key}, field: ${field}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希字段
|
||||
*/
|
||||
async hget<T = any>(key: string, field: string): Promise<T | null> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const value = await this.client.hGet(prefixedKey, field);
|
||||
|
||||
if (value === null) {
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.cacheStats.hits++;
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to hget cache key: ${key}, field: ${field}`,
|
||||
error,
|
||||
);
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除哈希字段
|
||||
*/
|
||||
async hdel(key: string, field: string): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
await this.client.hDel(prefixedKey, field);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to hdel cache key: ${key}, field: ${field}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希所有字段
|
||||
*/
|
||||
async hgetall(key: string): Promise<Record<string, any>> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const hash = await this.client.hGetAll(prefixedKey);
|
||||
|
||||
const result: Record<string, any> = {};
|
||||
for (const [field, value] of Object.entries(hash)) {
|
||||
result[field] = JSON.parse(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to hgetall cache key: ${key}`, error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息
|
||||
*/
|
||||
async publish(channel: string, message: any): Promise<number> {
|
||||
try {
|
||||
const serializedMessage = JSON.stringify(message);
|
||||
return await this.client.publish(channel, serializedMessage);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to publish message to channel: ${channel}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅频道
|
||||
*/
|
||||
async subscribe(
|
||||
channel: string,
|
||||
callback: (message: any) => void,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.client.subscribe(channel, (message) => {
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
callback(parsedMessage);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to parse message from channel: ${channel}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to subscribe to channel: ${channel}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*/
|
||||
async unsubscribe(channel: string): Promise<void> {
|
||||
try {
|
||||
await this.client.unsubscribe(channel);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to unsubscribe from channel: ${channel}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带前缀的键
|
||||
*/
|
||||
private getPrefixedKey(key: string): string {
|
||||
const prefix = this.config.keyPrefix || 'cache';
|
||||
return `${prefix}:${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除键前缀
|
||||
*/
|
||||
private removePrefix(key: string): string {
|
||||
const prefix = this.config.keyPrefix || 'cache';
|
||||
return key.startsWith(`${prefix}:`)
|
||||
? key.substring(prefix.length + 1)
|
||||
: key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始 Redis 客户端
|
||||
*/
|
||||
getClient(): RedisClientType {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接状态
|
||||
*/
|
||||
async isConnected(): Promise<boolean> {
|
||||
try {
|
||||
await this.client.ping();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重连
|
||||
*/
|
||||
async reconnect(): Promise<void> {
|
||||
try {
|
||||
if (this.client) {
|
||||
await this.client.quit();
|
||||
}
|
||||
await this.initializeClient();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to reconnect Redis client', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/common/libraries/sentry/sentry.module.ts
Normal file
26
src/common/libraries/sentry/sentry.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { SentryService } from './sentry.service';
|
||||
|
||||
/**
|
||||
* Sentry 错误追踪模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 错误追踪配置
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: SentryService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const sentryConfig = configService.get('sentry');
|
||||
return new SentryService(sentryConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [SentryService],
|
||||
})
|
||||
export class SentryModule {}
|
||||
687
src/common/libraries/sentry/sentry.service.ts
Normal file
687
src/common/libraries/sentry/sentry.service.ts
Normal file
@@ -0,0 +1,687 @@
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
ExceptionReporterInterface,
|
||||
ExceptionStatsInterface,
|
||||
ExceptionType,
|
||||
ExceptionSeverity,
|
||||
} from '../../exception/exception.interface';
|
||||
|
||||
/**
|
||||
* Sentry 服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 错误追踪服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class SentryService
|
||||
implements
|
||||
ExceptionReporterInterface,
|
||||
ExceptionStatsInterface,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy
|
||||
{
|
||||
private readonly logger = new Logger(SentryService.name);
|
||||
private initialized = false;
|
||||
private stats = new Map<string, number>();
|
||||
|
||||
constructor(private readonly config: any) {
|
||||
this.initializeSentry();
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
if (this.config?.enabled && !this.initialized) {
|
||||
try {
|
||||
Sentry.init({
|
||||
dsn: this.config.dsn,
|
||||
environment: this.config.environment || process.env.NODE_ENV,
|
||||
release: this.config.release || process.env.npm_package_version,
|
||||
tracesSampleRate: this.config.tracesSampleRate || 0.1,
|
||||
profilesSampleRate: this.config.profilesSampleRate || 0.1,
|
||||
beforeSend: this.beforeSend.bind(this),
|
||||
beforeBreadcrumb: this.beforeBreadcrumb.bind(this),
|
||||
integrations: [
|
||||
// 使用默认集成
|
||||
],
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
this.logger.log('Sentry initialized successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize Sentry', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.initialized) {
|
||||
try {
|
||||
await Sentry.close(2000);
|
||||
this.initialized = false;
|
||||
this.logger.log('Sentry closed successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to close Sentry', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initializeSentry() {
|
||||
// Sentry 初始化在 onModuleInit 中进行
|
||||
}
|
||||
|
||||
// ==================== 异常上报接口 ====================
|
||||
|
||||
/**
|
||||
* 上报异常
|
||||
*/
|
||||
async report(exception: any, context?: any): Promise<boolean> {
|
||||
if (!this.initialized || !this.shouldReport(exception)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Sentry.withScope((scope) => {
|
||||
// 设置标签
|
||||
this.setTags(scope, exception, context);
|
||||
|
||||
// 设置上下文
|
||||
this.setContext(scope, exception, context);
|
||||
|
||||
// 设置用户信息
|
||||
this.setUser(scope, context);
|
||||
|
||||
// 设置额外信息
|
||||
this.setExtra(scope, exception, context);
|
||||
|
||||
// 设置级别
|
||||
this.setLevel(scope, exception);
|
||||
|
||||
// 捕获异常
|
||||
Sentry.captureException(exception);
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to report exception to Sentry', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上报异常
|
||||
*/
|
||||
async reportBatch(
|
||||
exceptions: Array<{ exception: any; context?: any }>,
|
||||
): Promise<boolean[]> {
|
||||
const results: boolean[] = [];
|
||||
|
||||
for (const { exception, context } of exceptions) {
|
||||
const result = await this.report(exception, context);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该上报
|
||||
*/
|
||||
shouldReport(exception: any): boolean {
|
||||
if (!this.config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据异常类型和严重程度判断
|
||||
const type = this.getExceptionType(exception);
|
||||
const severity = this.getExceptionSeverity(exception);
|
||||
|
||||
// 只上报高严重程度的异常
|
||||
if (
|
||||
severity === ExceptionSeverity.HIGH ||
|
||||
severity === ExceptionSeverity.CRITICAL
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 某些类型的异常总是上报
|
||||
if (
|
||||
type === ExceptionType.SYSTEM ||
|
||||
type === ExceptionType.INTERNAL_SERVER_ERROR
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== 异常统计接口 ====================
|
||||
|
||||
/**
|
||||
* 记录异常统计
|
||||
*/
|
||||
record(exception: any, context?: any): void {
|
||||
const type = this.getExceptionType(exception);
|
||||
const severity = this.getExceptionSeverity(exception);
|
||||
|
||||
const key = `${type}:${severity}`;
|
||||
const count = this.stats.get(key) || 0;
|
||||
this.stats.set(key, count + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常统计
|
||||
*/
|
||||
getStats(timeRange?: { start: Date; end: Date }): any {
|
||||
const stats: any = {
|
||||
total: 0,
|
||||
byType: {},
|
||||
bySeverity: {},
|
||||
byTime: [],
|
||||
topExceptions: [],
|
||||
rate: 0,
|
||||
trend: 'stable',
|
||||
};
|
||||
|
||||
// 计算统计信息
|
||||
for (const [key, count] of this.stats) {
|
||||
const [type, severity] = key.split(':');
|
||||
stats.total += count;
|
||||
|
||||
stats.byType[type] = (stats.byType[type] || 0) + count;
|
||||
stats.bySeverity[severity] = (stats.bySeverity[severity] || 0) + count;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计
|
||||
*/
|
||||
reset(): void {
|
||||
this.stats.clear();
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 设置标签
|
||||
*/
|
||||
private setTags(scope: Sentry.Scope, exception: any, context?: any): void {
|
||||
const type = this.getExceptionType(exception);
|
||||
const severity = this.getExceptionSeverity(exception);
|
||||
|
||||
scope.setTag('exception.type', type);
|
||||
scope.setTag('exception.severity', severity);
|
||||
|
||||
if (context?.request) {
|
||||
scope.setTag('http.method', context.request.method);
|
||||
scope.setTag('http.url', context.request.url);
|
||||
scope.setTag('http.status_code', context.response?.statusCode);
|
||||
}
|
||||
|
||||
if (context?.user) {
|
||||
scope.setTag('user.id', context.user.id);
|
||||
scope.setTag('user.role', context.user.role);
|
||||
}
|
||||
|
||||
if (context?.environment) {
|
||||
scope.setTag('environment', context.environment.nodeEnv);
|
||||
scope.setTag('version', context.environment.version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置上下文
|
||||
*/
|
||||
private setContext(scope: Sentry.Scope, exception: any, context?: any): void {
|
||||
if (context?.request) {
|
||||
scope.setContext('request', {
|
||||
method: context.request.method,
|
||||
url: context.request.url,
|
||||
headers: this.sanitizeHeaders(context.request.headers),
|
||||
body: this.sanitizeBody(context.request.body),
|
||||
query: context.request.query,
|
||||
params: context.request.params,
|
||||
ip: context.request.ip,
|
||||
userAgent: context.request.userAgent,
|
||||
});
|
||||
}
|
||||
|
||||
if (context?.response) {
|
||||
scope.setContext('response', {
|
||||
statusCode: context.response.statusCode,
|
||||
headers: this.sanitizeHeaders(context.response.headers),
|
||||
body: this.sanitizeBody(context.response.body),
|
||||
size: context.response.size,
|
||||
});
|
||||
}
|
||||
|
||||
if (context?.environment) {
|
||||
scope.setContext('environment', {
|
||||
nodeEnv: context.environment.nodeEnv,
|
||||
version: context.environment.version,
|
||||
hostname: context.environment.hostname,
|
||||
pid: context.environment.pid,
|
||||
});
|
||||
}
|
||||
|
||||
if (context?.trace) {
|
||||
scope.setContext('trace', {
|
||||
traceId: context.trace.traceId,
|
||||
spanId: context.trace.spanId,
|
||||
correlationId: context.trace.correlationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
private setUser(scope: Sentry.Scope, context?: any): void {
|
||||
if (context?.user) {
|
||||
scope.setUser({
|
||||
id: context.user.id,
|
||||
username: context.user.username,
|
||||
email: context.user.email,
|
||||
role: context.user.role,
|
||||
permissions: context.user.permissions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置额外信息
|
||||
*/
|
||||
private setExtra(scope: Sentry.Scope, exception: any, context?: any): void {
|
||||
if (exception.code) {
|
||||
scope.setExtra('error.code', exception.code);
|
||||
}
|
||||
|
||||
if (exception.details) {
|
||||
scope.setExtra('error.details', exception.details);
|
||||
}
|
||||
|
||||
if (exception.cause) {
|
||||
scope.setExtra('error.cause', exception.cause);
|
||||
}
|
||||
|
||||
if (context?.meta) {
|
||||
scope.setExtra('meta', context.meta);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置级别
|
||||
*/
|
||||
private setLevel(scope: Sentry.Scope, exception: any): void {
|
||||
const severity = this.getExceptionSeverity(exception);
|
||||
|
||||
switch (severity) {
|
||||
case ExceptionSeverity.CRITICAL:
|
||||
scope.setLevel('fatal');
|
||||
break;
|
||||
case ExceptionSeverity.HIGH:
|
||||
scope.setLevel('error');
|
||||
break;
|
||||
case ExceptionSeverity.MEDIUM:
|
||||
scope.setLevel('warning');
|
||||
break;
|
||||
case ExceptionSeverity.LOW:
|
||||
scope.setLevel('info');
|
||||
break;
|
||||
default:
|
||||
scope.setLevel('error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常类型
|
||||
*/
|
||||
private getExceptionType(exception: any): ExceptionType {
|
||||
if (exception.name) {
|
||||
const name = exception.name.toLowerCase();
|
||||
|
||||
if (name.includes('validation') || name.includes('badrequest')) {
|
||||
return ExceptionType.VALIDATION;
|
||||
}
|
||||
if (name.includes('unauthorized') || name.includes('authentication')) {
|
||||
return ExceptionType.AUTHENTICATION;
|
||||
}
|
||||
if (name.includes('forbidden') || name.includes('authorization')) {
|
||||
return ExceptionType.AUTHORIZATION;
|
||||
}
|
||||
if (name.includes('notfound')) {
|
||||
return ExceptionType.NOT_FOUND;
|
||||
}
|
||||
if (name.includes('conflict')) {
|
||||
return ExceptionType.CONFLICT;
|
||||
}
|
||||
if (name.includes('timeout')) {
|
||||
return ExceptionType.TIMEOUT;
|
||||
}
|
||||
if (name.includes('ratelimit')) {
|
||||
return ExceptionType.RATE_LIMIT;
|
||||
}
|
||||
if (name.includes('database') || name.includes('db')) {
|
||||
return ExceptionType.DATABASE;
|
||||
}
|
||||
if (name.includes('cache')) {
|
||||
return ExceptionType.CACHE;
|
||||
}
|
||||
if (name.includes('network') || name.includes('connection')) {
|
||||
return ExceptionType.NETWORK;
|
||||
}
|
||||
if (name.includes('external') || name.includes('api')) {
|
||||
return ExceptionType.EXTERNAL_API;
|
||||
}
|
||||
if (name.includes('business')) {
|
||||
return ExceptionType.BUSINESS;
|
||||
}
|
||||
if (name.includes('system')) {
|
||||
return ExceptionType.SYSTEM;
|
||||
}
|
||||
}
|
||||
|
||||
if (exception.statusCode) {
|
||||
switch (exception.statusCode) {
|
||||
case 400:
|
||||
return ExceptionType.BAD_REQUEST;
|
||||
case 401:
|
||||
return ExceptionType.AUTHENTICATION;
|
||||
case 403:
|
||||
return ExceptionType.AUTHORIZATION;
|
||||
case 404:
|
||||
return ExceptionType.NOT_FOUND;
|
||||
case 409:
|
||||
return ExceptionType.CONFLICT;
|
||||
case 429:
|
||||
return ExceptionType.RATE_LIMIT;
|
||||
case 500:
|
||||
return ExceptionType.INTERNAL_SERVER_ERROR;
|
||||
case 503:
|
||||
return ExceptionType.SERVICE_UNAVAILABLE;
|
||||
default:
|
||||
return ExceptionType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
return ExceptionType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常严重程度
|
||||
*/
|
||||
private getExceptionSeverity(exception: any): ExceptionSeverity {
|
||||
const type = this.getExceptionType(exception);
|
||||
|
||||
switch (type) {
|
||||
case ExceptionType.INTERNAL_SERVER_ERROR:
|
||||
case ExceptionType.SYSTEM:
|
||||
return ExceptionSeverity.CRITICAL;
|
||||
case ExceptionType.AUTHENTICATION:
|
||||
case ExceptionType.AUTHORIZATION:
|
||||
case ExceptionType.DATABASE:
|
||||
case ExceptionType.NETWORK:
|
||||
return ExceptionSeverity.HIGH;
|
||||
case ExceptionType.VALIDATION:
|
||||
case ExceptionType.BAD_REQUEST:
|
||||
case ExceptionType.CONFLICT:
|
||||
case ExceptionType.CACHE:
|
||||
case ExceptionType.EXTERNAL_API:
|
||||
return ExceptionSeverity.MEDIUM;
|
||||
case ExceptionType.NOT_FOUND:
|
||||
case ExceptionType.TIMEOUT:
|
||||
case ExceptionType.RATE_LIMIT:
|
||||
case ExceptionType.BUSINESS:
|
||||
return ExceptionSeverity.LOW;
|
||||
default:
|
||||
return ExceptionSeverity.MEDIUM;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求头
|
||||
*/
|
||||
private sanitizeHeaders(
|
||||
headers: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
const sanitized: Record<string, string> = {};
|
||||
const sensitiveHeaders = [
|
||||
'authorization',
|
||||
'cookie',
|
||||
'x-api-key',
|
||||
'x-auth-token',
|
||||
];
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (sensitiveHeaders.includes(key.toLowerCase())) {
|
||||
sanitized[key] = '[REDACTED]';
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求体
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body) return body;
|
||||
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
return this.sanitizeObject(parsed);
|
||||
} catch {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'object') {
|
||||
return this.sanitizeObject(body);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理对象
|
||||
*/
|
||||
private sanitizeObject(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
const sanitized = { ...obj };
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送前处理
|
||||
*/
|
||||
private beforeSend(event: Sentry.Event): Sentry.Event | null {
|
||||
// 过滤敏感信息
|
||||
if (event.extra) {
|
||||
event.extra = this.sanitizeObject(event.extra);
|
||||
}
|
||||
|
||||
if (event.contexts) {
|
||||
event.contexts = this.sanitizeObject(event.contexts);
|
||||
}
|
||||
|
||||
if (event.tags) {
|
||||
event.tags = this.sanitizeObject(event.tags);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 面包屑前处理
|
||||
*/
|
||||
private beforeBreadcrumb(
|
||||
breadcrumb: Sentry.Breadcrumb,
|
||||
): Sentry.Breadcrumb | null {
|
||||
// 过滤敏感信息
|
||||
if (breadcrumb.data) {
|
||||
breadcrumb.data = this.sanitizeObject(breadcrumb.data);
|
||||
}
|
||||
|
||||
return breadcrumb;
|
||||
}
|
||||
|
||||
// ==================== 公共方法 ====================
|
||||
|
||||
/**
|
||||
* 手动捕获异常
|
||||
*/
|
||||
captureException(exception: any, context?: any): void {
|
||||
if (this.initialized) {
|
||||
Sentry.withScope((scope) => {
|
||||
this.setTags(scope, exception, context);
|
||||
this.setContext(scope, exception, context);
|
||||
this.setUser(scope, context);
|
||||
this.setExtra(scope, exception, context);
|
||||
this.setLevel(scope, exception);
|
||||
|
||||
Sentry.captureException(exception);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动捕获消息
|
||||
*/
|
||||
captureMessage(
|
||||
message: string,
|
||||
level: Sentry.SeverityLevel = 'info',
|
||||
context?: any,
|
||||
): void {
|
||||
if (this.initialized) {
|
||||
Sentry.withScope((scope) => {
|
||||
if (context) {
|
||||
this.setContext(scope, null, context);
|
||||
this.setUser(scope, context);
|
||||
this.setExtra(scope, null, context);
|
||||
}
|
||||
|
||||
scope.setLevel(level);
|
||||
Sentry.captureMessage(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始事务
|
||||
*/
|
||||
startTransaction(name: string, op: string): any {
|
||||
if (this.initialized) {
|
||||
// 新版本 Sentry API 已变更,暂时返回 null
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前事务
|
||||
*/
|
||||
getCurrentTransaction(): any {
|
||||
if (this.initialized) {
|
||||
// 新版本 Sentry API 已变更,暂时返回 undefined
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户上下文
|
||||
*/
|
||||
setUserContext(user: {
|
||||
id: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
}): void {
|
||||
if (this.initialized) {
|
||||
Sentry.setUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标签
|
||||
*/
|
||||
setTag(key: string, value: string): void {
|
||||
if (this.initialized) {
|
||||
Sentry.setTag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置上下文(公共方法)
|
||||
*/
|
||||
setContextPublic(key: string, context: any): void {
|
||||
if (this.initialized) {
|
||||
Sentry.setContext(key, context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置额外信息(公共方法)
|
||||
*/
|
||||
setExtraPublic(key: string, value: any): void {
|
||||
if (this.initialized) {
|
||||
Sentry.setExtra(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置级别(公共方法)
|
||||
*/
|
||||
setLevelPublic(level: Sentry.SeverityLevel): void {
|
||||
if (this.initialized) {
|
||||
// 新版本 Sentry API 已变更,暂时不执行
|
||||
// Sentry.setLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加面包屑
|
||||
*/
|
||||
addBreadcrumb(breadcrumb: Sentry.Breadcrumb): void {
|
||||
if (this.initialized) {
|
||||
Sentry.addBreadcrumb(breadcrumb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置作用域
|
||||
*/
|
||||
configureScope(callback: (scope: Sentry.Scope) => void): void {
|
||||
if (this.initialized) {
|
||||
// 新版本 Sentry API 已变更,暂时不执行
|
||||
// Sentry.configureScope(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用作用域
|
||||
*/
|
||||
withScope(callback: (scope: Sentry.Scope) => void): void {
|
||||
if (this.initialized) {
|
||||
Sentry.withScope(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/common/libraries/sharp/sharp.module.ts
Normal file
15
src/common/libraries/sharp/sharp.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { SharpService } from './sharp.service';
|
||||
|
||||
/**
|
||||
* Sharp 图片处理库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: ImageUtils
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [SharpService],
|
||||
exports: [SharpService],
|
||||
})
|
||||
export class SharpModule {}
|
||||
161
src/common/libraries/sharp/sharp.service.ts
Normal file
161
src/common/libraries/sharp/sharp.service.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import sharp from 'sharp';
|
||||
|
||||
/**
|
||||
* Sharp 图片处理服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: ImageUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class SharpService {
|
||||
/**
|
||||
* 获取 sharp 实例
|
||||
*/
|
||||
getSharp() {
|
||||
return sharp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整图片大小
|
||||
*/
|
||||
async resize(
|
||||
input: Buffer | string,
|
||||
width: number,
|
||||
height?: number,
|
||||
options?: sharp.ResizeOptions,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).resize(width, height, options).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缩略图
|
||||
*/
|
||||
async thumbnail(
|
||||
input: Buffer | string,
|
||||
size: number,
|
||||
options?: sharp.ResizeOptions,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input)
|
||||
.resize(size, size, { ...options, fit: 'cover' })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片
|
||||
*/
|
||||
async compress(
|
||||
input: Buffer | string,
|
||||
quality: number = 80,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).jpeg({ quality }).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换图片格式
|
||||
*/
|
||||
async convert(
|
||||
input: Buffer | string,
|
||||
format: 'jpeg' | 'png' | 'webp' | 'gif' | 'tiff',
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).toFormat(format).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片信息
|
||||
*/
|
||||
async metadata(input: Buffer | string): Promise<sharp.Metadata> {
|
||||
return sharp(input).metadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪图片
|
||||
*/
|
||||
async crop(
|
||||
input: Buffer | string,
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).extract({ left, top, width, height }).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转图片
|
||||
*/
|
||||
async rotate(input: Buffer | string, angle: number): Promise<Buffer> {
|
||||
return sharp(input).rotate(angle).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻转图片
|
||||
*/
|
||||
async flip(input: Buffer | string): Promise<Buffer> {
|
||||
return sharp(input).flip().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 镜像图片
|
||||
*/
|
||||
async flop(input: Buffer | string): Promise<Buffer> {
|
||||
return sharp(input).flop().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 模糊图片
|
||||
*/
|
||||
async blur(input: Buffer | string, sigma: number): Promise<Buffer> {
|
||||
return sharp(input).blur(sigma).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 锐化图片
|
||||
*/
|
||||
async sharpen(
|
||||
input: Buffer | string,
|
||||
sigma?: number,
|
||||
flat?: number,
|
||||
jagged?: number,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).sharpen(sigma, flat, jagged).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整亮度
|
||||
*/
|
||||
async modulate(
|
||||
input: Buffer | string,
|
||||
brightness: number = 1,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).modulate({ brightness }).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整对比度
|
||||
*/
|
||||
async linear(input: Buffer | string, a: number, b: number): Promise<Buffer> {
|
||||
return sharp(input).linear(a, b).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加水印
|
||||
*/
|
||||
async composite(
|
||||
input: Buffer | string,
|
||||
overlay: Buffer | string,
|
||||
options?: sharp.OverlayOptions,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input)
|
||||
.composite([{ input: overlay, ...options }])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图片哈希
|
||||
*/
|
||||
async hash(input: Buffer | string): Promise<string> {
|
||||
const metadata = await sharp(input).metadata();
|
||||
return `${metadata.width}x${metadata.height}-${metadata.format}`;
|
||||
}
|
||||
}
|
||||
15
src/common/libraries/uuid/uuid.module.ts
Normal file
15
src/common/libraries/uuid/uuid.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { UuidService } from './uuid.service';
|
||||
|
||||
/**
|
||||
* UUID 生成库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: IdUtil
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [UuidService],
|
||||
exports: [UuidService],
|
||||
})
|
||||
export class UuidModule {}
|
||||
105
src/common/libraries/uuid/uuid.service.ts
Normal file
105
src/common/libraries/uuid/uuid.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
// 使用 crypto 模块替代 uuid 库以避免 ES module 问题
|
||||
import { randomUUID, createHash } from 'crypto';
|
||||
|
||||
/**
|
||||
* UUID 生成服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: IdUtil
|
||||
*/
|
||||
@Injectable()
|
||||
export class UuidService {
|
||||
/**
|
||||
* 生成 UUID v4 (随机)
|
||||
*/
|
||||
v4(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 UUID v1 (基于时间戳,简化实现)
|
||||
*/
|
||||
v1(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 UUID v5 (基于 SHA-1 哈希)
|
||||
*/
|
||||
v5(name: string, namespace: string): string {
|
||||
const hash = createHash('sha1');
|
||||
hash.update(namespace + name);
|
||||
const hex = hash.digest('hex');
|
||||
return (
|
||||
hex.substring(0, 8) +
|
||||
'-' +
|
||||
hex.substring(8, 12) +
|
||||
'-5' +
|
||||
hex.substring(12, 15) +
|
||||
'-' +
|
||||
hex.substring(15, 19) +
|
||||
'-' +
|
||||
hex.substring(19, 31)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 UUID v3 (基于 MD5 哈希)
|
||||
*/
|
||||
v3(name: string, namespace: string): string {
|
||||
const hash = createHash('md5');
|
||||
hash.update(namespace + name);
|
||||
const hex = hash.digest('hex');
|
||||
return (
|
||||
hex.substring(0, 8) +
|
||||
'-' +
|
||||
hex.substring(8, 12) +
|
||||
'-3' +
|
||||
hex.substring(12, 15) +
|
||||
'-' +
|
||||
hex.substring(15, 19) +
|
||||
'-' +
|
||||
hex.substring(19, 31)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 UUID 格式
|
||||
*/
|
||||
validate(uuid: string): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UUID 版本
|
||||
*/
|
||||
version(uuid: string): number | undefined {
|
||||
if (!this.validate(uuid)) return undefined;
|
||||
const version = uuid.charAt(14);
|
||||
return parseInt(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成短 UUID (8位)
|
||||
*/
|
||||
short(): string {
|
||||
return randomUUID().replace(/-/g, '').substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成中 UUID (16位)
|
||||
*/
|
||||
medium(): string {
|
||||
return randomUUID().replace(/-/g, '').substring(0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成长 UUID (32位,无连字符)
|
||||
*/
|
||||
long(): string {
|
||||
return randomUUID().replace(/-/g, '');
|
||||
}
|
||||
}
|
||||
15
src/common/libraries/validator/validator.module.ts
Normal file
15
src/common/libraries/validator/validator.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ValidatorService } from './validator.service';
|
||||
|
||||
/**
|
||||
* Validator 验证库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 验证工具类
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [ValidatorService],
|
||||
exports: [ValidatorService],
|
||||
})
|
||||
export class ValidatorModule {}
|
||||
628
src/common/libraries/validator/validator.service.ts
Normal file
628
src/common/libraries/validator/validator.service.ts
Normal file
@@ -0,0 +1,628 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as validator from 'validator';
|
||||
|
||||
/**
|
||||
* Validator 验证服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 验证工具类
|
||||
*/
|
||||
@Injectable()
|
||||
export class ValidatorService {
|
||||
/**
|
||||
* 获取 validator 实例
|
||||
*/
|
||||
getValidator() {
|
||||
return validator;
|
||||
}
|
||||
|
||||
// ==================== 字符串验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为邮箱
|
||||
*/
|
||||
isEmail(str: string): boolean {
|
||||
return validator.isEmail(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为URL
|
||||
*/
|
||||
isURL(str: string, options?: validator.IsURLOptions): boolean {
|
||||
return validator.isURL(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IP地址
|
||||
*/
|
||||
isIP(str: string, version?: validator.IPVersion): boolean {
|
||||
return validator.isIP(str, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IPv4地址
|
||||
*/
|
||||
isIPv4(str: string): boolean {
|
||||
return validator.isIP(str, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IPv6地址
|
||||
*/
|
||||
isIPv6(str: string): boolean {
|
||||
return validator.isIP(str, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为手机号
|
||||
*/
|
||||
isMobilePhone(str: string, locale?: validator.MobilePhoneLocale): boolean {
|
||||
return validator.isMobilePhone(str, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为中文手机号
|
||||
*/
|
||||
isMobilePhoneZh(str: string): boolean {
|
||||
return validator.isMobilePhone(str, 'zh-CN');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为身份证号
|
||||
*/
|
||||
isIdentityCard(str: string, locale?: validator.IdentityCardLocale): boolean {
|
||||
return validator.isIdentityCard(str, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为中文身份证号
|
||||
*/
|
||||
isIdentityCardZh(str: string): boolean {
|
||||
return validator.isIdentityCard(str, 'zh-CN');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为护照号
|
||||
*/
|
||||
isPassportNumber(str: string, countryCode?: string): boolean {
|
||||
// 简化实现,只检查基本格式
|
||||
return /^[A-Z0-9]{6,12}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为车牌号
|
||||
*/
|
||||
isLicensePlate(str: string, locale?: string): boolean {
|
||||
if (!locale) {
|
||||
// 通用车牌号格式检查
|
||||
return /^[A-Z0-9]{5,8}$/i.test(str);
|
||||
}
|
||||
return Boolean(validator.isLicensePlate(str, locale));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为中文车牌号
|
||||
*/
|
||||
isLicensePlateZh(str: string): boolean {
|
||||
// 中国车牌号格式:省份简称 + 字母 + 5位数字/字母
|
||||
return /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/.test(
|
||||
str,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 数字验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为数字
|
||||
*/
|
||||
isNumeric(str: string): boolean {
|
||||
return validator.isNumeric(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为整数
|
||||
*/
|
||||
isInt(str: string, options?: validator.IsIntOptions): boolean {
|
||||
return validator.isInt(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为浮点数
|
||||
*/
|
||||
isFloat(str: string, options?: validator.IsFloatOptions): boolean {
|
||||
return validator.isFloat(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为十六进制
|
||||
*/
|
||||
isHexadecimal(str: string): boolean {
|
||||
return validator.isHexadecimal(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为八进制
|
||||
*/
|
||||
isOctal(str: string): boolean {
|
||||
return validator.isOctal(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为二进制
|
||||
*/
|
||||
isBinary(str: string): boolean {
|
||||
// 检查是否为二进制字符串(只包含0和1)
|
||||
return /^[01]+$/.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为UUID
|
||||
*/
|
||||
isUUID(str: string, version?: validator.UUIDVersion): boolean {
|
||||
return validator.isUUID(str, version);
|
||||
}
|
||||
|
||||
// ==================== 字符串格式验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为字母
|
||||
*/
|
||||
isAlpha(str: string, locale?: validator.AlphaLocale): boolean {
|
||||
return validator.isAlpha(str, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为字母数字
|
||||
*/
|
||||
isAlphanumeric(str: string, locale?: validator.AlphanumericLocale): boolean {
|
||||
return validator.isAlphanumeric(str, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为ASCII
|
||||
*/
|
||||
isAscii(str: string): boolean {
|
||||
return validator.isAscii(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Base64
|
||||
*/
|
||||
isBase64(str: string): boolean {
|
||||
return validator.isBase64(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Base32
|
||||
*/
|
||||
isBase32(str: string): boolean {
|
||||
return validator.isBase32(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Base58
|
||||
*/
|
||||
isBase58(str: string): boolean {
|
||||
return validator.isBase58(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Base64URL
|
||||
*/
|
||||
isBase64URL(str: string): boolean {
|
||||
// Base64URL 格式检查(不包含 +、/、= 字符)
|
||||
return /^[A-Za-z0-9_-]+$/.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为十六进制颜色
|
||||
*/
|
||||
isHexColor(str: string): boolean {
|
||||
return validator.isHexColor(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为RGB颜色
|
||||
*/
|
||||
isRgbColor(str: string, includePercentValues?: boolean): boolean {
|
||||
// RGB 颜色格式检查
|
||||
const rgbPattern =
|
||||
/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
|
||||
const rgbaPattern =
|
||||
/^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0\.\d+)\s*\)$/;
|
||||
return rgbPattern.test(str) || rgbaPattern.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为HSL颜色
|
||||
*/
|
||||
isHslColor(str: string): boolean {
|
||||
// HSL 颜色格式检查
|
||||
const hslPattern =
|
||||
/^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/;
|
||||
const hslaPattern =
|
||||
/^hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*(0|1|0\.\d+)\s*\)$/;
|
||||
return hslPattern.test(str) || hslaPattern.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为JSON
|
||||
*/
|
||||
isJSON(str: string): boolean {
|
||||
return validator.isJSON(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为JWT
|
||||
*/
|
||||
isJWT(str: string): boolean {
|
||||
return validator.isJWT(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为MongoDB ObjectId
|
||||
*/
|
||||
isMongoId(str: string): boolean {
|
||||
return validator.isMongoId(str);
|
||||
}
|
||||
|
||||
// ==================== 长度验证 ====================
|
||||
|
||||
/**
|
||||
* 判断长度是否在范围内
|
||||
*/
|
||||
isLength(str: string, options?: validator.IsLengthOptions): boolean {
|
||||
return validator.isLength(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为空
|
||||
*/
|
||||
isEmpty(str: string): boolean {
|
||||
return validator.isEmpty(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否不为空
|
||||
*/
|
||||
isNotEmpty(str: string): boolean {
|
||||
return !validator.isEmpty(str);
|
||||
}
|
||||
|
||||
// ==================== 日期验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为日期
|
||||
*/
|
||||
isDate(str: string): boolean {
|
||||
return validator.isDate(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为ISO日期
|
||||
*/
|
||||
isISO8601(str: string, options?: validator.IsISO8601Options): boolean {
|
||||
return validator.isISO8601(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为RFC 3339日期
|
||||
*/
|
||||
isRFC3339(str: string): boolean {
|
||||
return validator.isRFC3339(str);
|
||||
}
|
||||
|
||||
// ==================== 网络验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为域名
|
||||
*/
|
||||
isFQDN(str: string, options?: validator.IsFQDNOptions): boolean {
|
||||
return validator.isFQDN(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为MAC地址
|
||||
*/
|
||||
isMACAddress(str: string): boolean {
|
||||
return validator.isMACAddress(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为端口号
|
||||
*/
|
||||
isPort(str: string): boolean {
|
||||
return validator.isPort(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为数据URI
|
||||
*/
|
||||
isDataURI(str: string): boolean {
|
||||
return validator.isDataURI(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Magnet URI
|
||||
*/
|
||||
isMagnetURI(str: string): boolean {
|
||||
return validator.isMagnetURI(str);
|
||||
}
|
||||
|
||||
// ==================== 文件验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为文件扩展名
|
||||
*/
|
||||
isFileExtension(str: string, extensions?: string[]): boolean {
|
||||
// 文件扩展名检查
|
||||
if (!extensions || extensions.length === 0) {
|
||||
return /\.\w+$/.test(str);
|
||||
}
|
||||
const ext = str.toLowerCase().split('.').pop();
|
||||
return ext ? extensions.map((e) => e.toLowerCase()).includes(ext) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为MIME类型
|
||||
*/
|
||||
isMimeType(str: string): boolean {
|
||||
return validator.isMimeType(str);
|
||||
}
|
||||
|
||||
// ==================== 其他验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为信用卡号
|
||||
*/
|
||||
isCreditCard(str: string): boolean {
|
||||
return validator.isCreditCard(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IBAN
|
||||
*/
|
||||
isIBAN(str: string): boolean {
|
||||
return validator.isIBAN(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为BIC
|
||||
*/
|
||||
isBIC(str: string): boolean {
|
||||
return validator.isBIC(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为MD5
|
||||
*/
|
||||
isMD5(str: string): boolean {
|
||||
return validator.isMD5(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为SHA1
|
||||
*/
|
||||
isSHA1(str: string): boolean {
|
||||
// SHA1 哈希值检查(40位十六进制)
|
||||
return /^[a-f0-9]{40}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为SHA256
|
||||
*/
|
||||
isSHA256(str: string): boolean {
|
||||
// SHA256 哈希值检查(64位十六进制)
|
||||
return /^[a-f0-9]{64}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为SHA384
|
||||
*/
|
||||
isSHA384(str: string): boolean {
|
||||
// SHA384 哈希值检查(96位十六进制)
|
||||
return /^[a-f0-9]{96}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为SHA512
|
||||
*/
|
||||
isSHA512(str: string): boolean {
|
||||
// SHA512 哈希值检查(128位十六进制)
|
||||
return /^[a-f0-9]{128}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为强密码
|
||||
*/
|
||||
isStrongPassword(str: string, options?: any): boolean {
|
||||
return validator.isStrongPassword(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为时间
|
||||
*/
|
||||
isTime(str: string, options?: validator.IsTimeOptions): boolean {
|
||||
return validator.isTime(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为税号
|
||||
*/
|
||||
isTaxID(str: string, locale?: string): boolean {
|
||||
// 简化实现,只检查基本格式
|
||||
return /^[A-Z0-9]{8,20}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为中文税号
|
||||
*/
|
||||
isTaxIDZh(str: string): boolean {
|
||||
return validator.isTaxID(str, 'zh-CN');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为VAT
|
||||
*/
|
||||
isVAT(str: string, countryCode?: string): boolean {
|
||||
// 简化实现,只检查基本格式
|
||||
return /^[A-Z0-9]{8,15}$/i.test(str);
|
||||
}
|
||||
|
||||
// ==================== 转换方法 ====================
|
||||
|
||||
/**
|
||||
* 转换为布尔值
|
||||
*/
|
||||
toBoolean(str: string, strict?: boolean): boolean {
|
||||
return validator.toBoolean(str, strict);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为日期
|
||||
*/
|
||||
toDate(str: string): Date | null {
|
||||
const result = validator.toDate(str);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为浮点数
|
||||
*/
|
||||
toFloat(str: string): number {
|
||||
return validator.toFloat(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为整数
|
||||
*/
|
||||
toInt(str: string, radix?: number): number {
|
||||
return validator.toInt(str, radix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为字符串
|
||||
*/
|
||||
toString(input: any): string {
|
||||
return validator.toString(input);
|
||||
}
|
||||
|
||||
// ==================== 清理方法 ====================
|
||||
|
||||
/**
|
||||
* 清理HTML
|
||||
*/
|
||||
escape(str: string): string {
|
||||
return validator.escape(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转义HTML
|
||||
*/
|
||||
unescape(str: string): string {
|
||||
return validator.unescape(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理字符串
|
||||
*/
|
||||
stripLow(str: string, keepNewLines?: boolean): string {
|
||||
return validator.stripLow(str, keepNewLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理白名单字符
|
||||
*/
|
||||
whitelist(str: string, chars: string): string {
|
||||
return validator.whitelist(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理黑名单字符
|
||||
*/
|
||||
blacklist(str: string, chars: string): string {
|
||||
return validator.blacklist(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化邮箱
|
||||
*/
|
||||
normalizeEmail(
|
||||
email: string,
|
||||
options?: validator.NormalizeEmailOptions,
|
||||
): string | false {
|
||||
return validator.normalizeEmail(email, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化URL
|
||||
*/
|
||||
normalizeUrl(url: string, options?: any): string | false {
|
||||
// 简化实现,只做基本的URL规范化
|
||||
try {
|
||||
const normalized = new URL(url);
|
||||
return normalized.toString();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取字符串长度
|
||||
*/
|
||||
getLength(str: string): number {
|
||||
// 获取字符串长度
|
||||
return str.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串字节长度
|
||||
*/
|
||||
getByteLength(str: string): number {
|
||||
// 获取字符串字节长度(UTF-8编码)
|
||||
return Buffer.byteLength(str, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串字符长度
|
||||
*/
|
||||
getCharLength(str: string): number {
|
||||
// 获取字符串字符长度(考虑Unicode字符)
|
||||
return [...str].length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串单词长度
|
||||
*/
|
||||
getWordLength(str: string): number {
|
||||
// 获取字符串单词长度
|
||||
return str.split(/\s+/).filter((word) => word.length > 0).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串行数
|
||||
*/
|
||||
getLineLength(str: string): number {
|
||||
// 获取字符串行数
|
||||
return str.split('\n').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串段落数
|
||||
*/
|
||||
getParagraphLength(str: string): number {
|
||||
// 获取字符串段落数
|
||||
return str.split(/\n\s*\n/).filter((para) => para.trim().length > 0).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串句子数
|
||||
*/
|
||||
getSentenceLength(str: string): number {
|
||||
// 获取字符串句子数
|
||||
return str.split(/[.!?]+/).filter((sentence) => sentence.trim().length > 0)
|
||||
.length;
|
||||
}
|
||||
}
|
||||
26
src/common/libraries/winston/winston.module.ts
Normal file
26
src/common/libraries/winston/winston.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { WinstonService } from './winston.service';
|
||||
|
||||
/**
|
||||
* Winston 日志库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 日志配置
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: WinstonService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const winstonConfig = configService.get('winston');
|
||||
return new WinstonService(winstonConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [WinstonService],
|
||||
})
|
||||
export class WinstonModule {}
|
||||
576
src/common/libraries/winston/winston.service.ts
Normal file
576
src/common/libraries/winston/winston.service.ts
Normal file
@@ -0,0 +1,576 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as winston from 'winston';
|
||||
import type {
|
||||
LoggingInterface,
|
||||
StructuredLoggingInterface,
|
||||
StructuredLogData,
|
||||
RequestLogData,
|
||||
ResponseLogData,
|
||||
DatabaseQueryLogData,
|
||||
CacheOperationLogData,
|
||||
ExternalApiCallLogData,
|
||||
BusinessEventLogData,
|
||||
UserLogData,
|
||||
LoggingConfig,
|
||||
} from '../../logging/logging.interface';
|
||||
import { LogLevel } from '../../logging/logging.interface';
|
||||
|
||||
/**
|
||||
* Winston 服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 日志服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class WinstonService
|
||||
implements LoggingInterface, StructuredLoggingInterface
|
||||
{
|
||||
private readonly logger = new Logger(WinstonService.name);
|
||||
private winston: winston.Logger;
|
||||
private currentLevel: LogLevel = LogLevel.INFO;
|
||||
|
||||
constructor(
|
||||
private readonly config: LoggingConfig = {
|
||||
level: LogLevel.INFO,
|
||||
format: 'json',
|
||||
timestamp: true,
|
||||
colorize: false,
|
||||
prettyPrint: false,
|
||||
silent: false,
|
||||
exitOnError: false,
|
||||
transports: [{ type: 'console', level: LogLevel.INFO }],
|
||||
defaultMeta: {},
|
||||
context: 'WinstonService',
|
||||
},
|
||||
) {
|
||||
this.initializeWinston();
|
||||
}
|
||||
|
||||
private initializeWinston() {
|
||||
const transports: winston.transport[] = [];
|
||||
|
||||
// 控制台传输器
|
||||
if (this.config.transports.some((t) => t.type === 'console')) {
|
||||
const consoleTransport = new winston.transports.Console({
|
||||
level: this.config.level,
|
||||
format: this.getFormat('console'),
|
||||
});
|
||||
transports.push(consoleTransport);
|
||||
}
|
||||
|
||||
// 文件传输器
|
||||
const fileTransports = this.config.transports.filter(
|
||||
(t) => t.type === 'file',
|
||||
);
|
||||
for (const fileTransport of fileTransports) {
|
||||
const transport = new winston.transports.File({
|
||||
level: fileTransport.level || this.config.level,
|
||||
filename: fileTransport.options?.filename || 'app.log',
|
||||
format: this.getFormat('file'),
|
||||
...fileTransport.options,
|
||||
});
|
||||
transports.push(transport);
|
||||
}
|
||||
|
||||
// HTTP传输器
|
||||
const httpTransports = this.config.transports.filter(
|
||||
(t) => t.type === 'http',
|
||||
);
|
||||
for (const httpTransport of httpTransports) {
|
||||
const transport = new winston.transports.Http({
|
||||
level: httpTransport.level || this.config.level,
|
||||
host: httpTransport.options?.host || 'localhost',
|
||||
port: httpTransport.options?.port || 80,
|
||||
path: httpTransport.options?.path || '/logs',
|
||||
...httpTransport.options,
|
||||
});
|
||||
transports.push(transport);
|
||||
}
|
||||
|
||||
// 流传输器
|
||||
const streamTransports = this.config.transports.filter(
|
||||
(t) => t.type === 'stream',
|
||||
);
|
||||
for (const streamTransport of streamTransports) {
|
||||
const transport = new winston.transports.Stream({
|
||||
level: streamTransport.level || this.config.level,
|
||||
stream: streamTransport.options?.stream,
|
||||
format: this.getFormat('stream'),
|
||||
...streamTransport.options,
|
||||
});
|
||||
transports.push(transport);
|
||||
}
|
||||
|
||||
this.winston = winston.createLogger({
|
||||
level: this.config.level,
|
||||
format: this.getFormat('default'),
|
||||
defaultMeta: this.config.defaultMeta,
|
||||
transports,
|
||||
silent: this.config.silent,
|
||||
exitOnError: this.config.exitOnError,
|
||||
});
|
||||
}
|
||||
|
||||
private getFormat(type: string): winston.Logform.Format {
|
||||
const formats: winston.Logform.Format[] = [];
|
||||
|
||||
// 时间戳
|
||||
if (this.config.timestamp) {
|
||||
formats.push(winston.format.timestamp());
|
||||
}
|
||||
|
||||
// 日志级别
|
||||
// winston.format.level() 在新版本中已移除
|
||||
|
||||
// 消息格式
|
||||
if (this.config.format === 'json') {
|
||||
formats.push(winston.format.json());
|
||||
} else if (this.config.format === 'simple') {
|
||||
formats.push(winston.format.simple());
|
||||
} else {
|
||||
formats.push(
|
||||
winston.format.printf(
|
||||
({ timestamp, level, message, context, ...meta }) => {
|
||||
const contextStr = context ? `[${context}] ` : '';
|
||||
const metaStr =
|
||||
Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
|
||||
return `${timestamp} [${level}] ${contextStr}${message}${metaStr}`;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 颜色
|
||||
if (this.config.colorize && type === 'console') {
|
||||
formats.push(winston.format.colorize());
|
||||
}
|
||||
|
||||
// 美化打印
|
||||
if (this.config.prettyPrint) {
|
||||
formats.push(winston.format.prettyPrint());
|
||||
}
|
||||
|
||||
return winston.format.combine(...formats);
|
||||
}
|
||||
|
||||
// ==================== 基础日志接口 ====================
|
||||
|
||||
/**
|
||||
* 记录调试日志
|
||||
*/
|
||||
debug(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.debug(message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录信息日志
|
||||
*/
|
||||
info(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.info(message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录警告日志
|
||||
*/
|
||||
warn(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.warn(message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*/
|
||||
error(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.error(message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录致命错误日志
|
||||
*/
|
||||
fatal(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.error(message, { context, level: 'fatal', ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*/
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: string,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
this.winston.log(level, message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志级别
|
||||
*/
|
||||
setLevel(level: LogLevel): void {
|
||||
this.currentLevel = level;
|
||||
this.winston.level = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日志级别
|
||||
*/
|
||||
getLevel(): LogLevel {
|
||||
return this.currentLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建子日志器
|
||||
*/
|
||||
child(context: string): LoggingInterface {
|
||||
const childLogger = this.winston.child({ context });
|
||||
return new WinstonChildService(childLogger, context);
|
||||
}
|
||||
|
||||
// ==================== 结构化日志接口 ====================
|
||||
|
||||
/**
|
||||
* 记录结构化日志
|
||||
*/
|
||||
logStructured(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
structuredData: StructuredLogData,
|
||||
): void {
|
||||
this.winston.log(level, message, structuredData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求日志
|
||||
*/
|
||||
logRequest(
|
||||
request: RequestLogData,
|
||||
response: ResponseLogData,
|
||||
duration: number,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'http_request',
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: this.sanitizeHeaders(request.headers),
|
||||
body: this.sanitizeBody(request.body),
|
||||
query: request.query,
|
||||
params: request.params,
|
||||
ip: request.ip,
|
||||
userAgent: request.userAgent,
|
||||
userId: request.userId,
|
||||
sessionId: request.sessionId,
|
||||
},
|
||||
response: {
|
||||
statusCode: response.statusCode,
|
||||
headers: this.sanitizeHeaders(response.headers),
|
||||
body: this.sanitizeBody(response.body),
|
||||
size: response.size,
|
||||
},
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('HTTP Request', logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据库查询日志
|
||||
*/
|
||||
logDatabaseQuery(
|
||||
query: DatabaseQueryLogData,
|
||||
duration: number,
|
||||
result?: any,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'database_query',
|
||||
query: {
|
||||
operation: query.operation,
|
||||
table: query.table,
|
||||
query: query.query,
|
||||
params: query.params,
|
||||
connection: query.connection,
|
||||
transaction: query.transaction,
|
||||
},
|
||||
duration,
|
||||
result: this.sanitizeResult(result),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('Database Query', logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存操作日志
|
||||
*/
|
||||
logCacheOperation(
|
||||
operation: CacheOperationLogData,
|
||||
hit: boolean,
|
||||
duration: number,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'cache_operation',
|
||||
operation: {
|
||||
operation: operation.operation,
|
||||
key: operation.key,
|
||||
ttl: operation.ttl,
|
||||
size: operation.size,
|
||||
},
|
||||
hit,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('Cache Operation', logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录外部API调用日志
|
||||
*/
|
||||
logExternalApiCall(
|
||||
apiCall: ExternalApiCallLogData,
|
||||
response: any,
|
||||
duration: number,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'external_api_call',
|
||||
apiCall: {
|
||||
service: apiCall.service,
|
||||
endpoint: apiCall.endpoint,
|
||||
method: apiCall.method,
|
||||
headers: this.sanitizeHeaders(apiCall.headers),
|
||||
body: this.sanitizeBody(apiCall.body),
|
||||
timeout: apiCall.timeout,
|
||||
},
|
||||
response: this.sanitizeResponse(response),
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('External API Call', logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录业务事件日志
|
||||
*/
|
||||
logBusinessEvent(
|
||||
event: BusinessEventLogData,
|
||||
user?: UserLogData,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'business_event',
|
||||
event: {
|
||||
event: event.event,
|
||||
action: event.action,
|
||||
resource: event.resource,
|
||||
resourceId: event.resourceId,
|
||||
data: event.data,
|
||||
result: event.result,
|
||||
},
|
||||
user: user
|
||||
? {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
permissions: user.permissions,
|
||||
}
|
||||
: undefined,
|
||||
meta,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('Business Event', logData);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 清理请求头
|
||||
*/
|
||||
private sanitizeHeaders(
|
||||
headers: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
const sanitized: Record<string, string> = {};
|
||||
const sensitiveHeaders = [
|
||||
'authorization',
|
||||
'cookie',
|
||||
'x-api-key',
|
||||
'x-auth-token',
|
||||
];
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (sensitiveHeaders.includes(key.toLowerCase())) {
|
||||
sanitized[key] = '[REDACTED]';
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求体
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body) return body;
|
||||
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
return this.sanitizeObject(parsed);
|
||||
} catch {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'object') {
|
||||
return this.sanitizeObject(body);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理对象
|
||||
*/
|
||||
private sanitizeObject(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
const sanitized = { ...obj };
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理结果
|
||||
*/
|
||||
private sanitizeResult(result: any): any {
|
||||
if (!result) return result;
|
||||
|
||||
if (typeof result === 'object') {
|
||||
return this.sanitizeObject(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理响应
|
||||
*/
|
||||
private sanitizeResponse(response: any): any {
|
||||
if (!response) return response;
|
||||
|
||||
if (typeof response === 'object') {
|
||||
return this.sanitizeObject(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始 Winston 实例
|
||||
*/
|
||||
getWinston(): winston.Logger {
|
||||
return this.winston;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加传输器
|
||||
*/
|
||||
addTransport(transport: winston.transport): void {
|
||||
this.winston.add(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除传输器
|
||||
*/
|
||||
removeTransport(transport: winston.transport): void {
|
||||
this.winston.remove(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空传输器
|
||||
*/
|
||||
clearTransports(): void {
|
||||
this.winston.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭日志器
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.winston.end(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Winston 子日志器服务
|
||||
*/
|
||||
class WinstonChildService implements LoggingInterface {
|
||||
constructor(
|
||||
private readonly winston: winston.Logger,
|
||||
private readonly context: string,
|
||||
) {}
|
||||
|
||||
debug(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.debug(message, { context: context || this.context, ...meta });
|
||||
}
|
||||
|
||||
info(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.info(message, { context: context || this.context, ...meta });
|
||||
}
|
||||
|
||||
warn(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.warn(message, { context: context || this.context, ...meta });
|
||||
}
|
||||
|
||||
error(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.error(message, { context: context || this.context, ...meta });
|
||||
}
|
||||
|
||||
fatal(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.error(message, {
|
||||
context: context || this.context,
|
||||
level: 'fatal',
|
||||
...meta,
|
||||
});
|
||||
}
|
||||
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: string,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
this.winston.log(level, message, {
|
||||
context: context || this.context,
|
||||
...meta,
|
||||
});
|
||||
}
|
||||
|
||||
setLevel(level: LogLevel): void {
|
||||
this.winston.level = level;
|
||||
}
|
||||
|
||||
getLevel(): LogLevel {
|
||||
return this.winston.level as LogLevel;
|
||||
}
|
||||
|
||||
child(context: string): LoggingInterface {
|
||||
const childLogger = this.winston.child({ context });
|
||||
return new WinstonChildService(childLogger, context);
|
||||
}
|
||||
}
|
||||
14
src/common/loader/loader.module.ts
Normal file
14
src/common/loader/loader.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { LoaderUtils } from './loader.utils';
|
||||
|
||||
/**
|
||||
* 加载器模块 - 基础设施层
|
||||
* 基于 NestJS 实现 Java 风格的 JsonModuleLoader
|
||||
* 对应 Java: JsonModuleLoader
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [LoaderUtils],
|
||||
exports: [LoaderUtils],
|
||||
})
|
||||
export class LoaderModule {}
|
||||
244
src/common/loader/loader.utils.ts
Normal file
244
src/common/loader/loader.utils.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as fsSync from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 加载器工具类
|
||||
* 基于 NestJS 实现 Java 风格的 JsonModuleLoader
|
||||
* 对应 Java: JsonModuleLoader
|
||||
*/
|
||||
@Injectable()
|
||||
export class LoaderUtils {
|
||||
private readonly logger = new Logger(LoaderUtils.name);
|
||||
private readonly moduleCache = new Map<string, any>();
|
||||
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 加载 JSON 模块
|
||||
* @param moduleName 模块名称
|
||||
* @param filePath 文件路径
|
||||
* @returns 模块内容
|
||||
*/
|
||||
async loadJsonModule(moduleName: string, filePath: string): Promise<any> {
|
||||
try {
|
||||
const fullPath = path.resolve(filePath);
|
||||
|
||||
try {
|
||||
await fs.access(fullPath);
|
||||
} catch (error) {
|
||||
this.logger.warn(`文件不存在: ${fullPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = JSON.parse(await fs.readFile(fullPath, 'utf8'));
|
||||
this.moduleCache.set(moduleName, content);
|
||||
|
||||
this.logger.log(`加载 JSON 模块成功: ${moduleName}`);
|
||||
return content;
|
||||
} catch (error) {
|
||||
this.logger.error(`加载 JSON 模块失败: ${moduleName}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载多个 JSON 模块并合并为数组
|
||||
* @param moduleName 模块名称
|
||||
* @param filePaths 文件路径数组
|
||||
* @returns 合并后的数组
|
||||
*/
|
||||
async mergeContentToArray(
|
||||
moduleName: string,
|
||||
filePaths: string[],
|
||||
): Promise<any[]> {
|
||||
try {
|
||||
const results: any[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const content = await this.loadJsonModule(
|
||||
`${moduleName}_${path.basename(filePath)}`,
|
||||
filePath,
|
||||
);
|
||||
if (content) {
|
||||
if (Array.isArray(content)) {
|
||||
results.push(...content);
|
||||
} else {
|
||||
results.push(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`合并内容到数组成功: ${moduleName}, 共 ${results.length} 项`,
|
||||
);
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error(`合并内容到数组失败: ${moduleName}`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载多个 JSON 模块并合并为对象
|
||||
* @param moduleName 模块名称
|
||||
* @param filePaths 文件路径数组
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
async mergeContentToObject(
|
||||
moduleName: string,
|
||||
filePaths: string[],
|
||||
): Promise<Record<string, any>> {
|
||||
try {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const content = await this.loadJsonModule(
|
||||
`${moduleName}_${path.basename(filePath)}`,
|
||||
filePath,
|
||||
);
|
||||
if (content && typeof content === 'object') {
|
||||
Object.assign(result, content);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`合并内容到对象成功: ${moduleName}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`合并内容到对象失败: ${moduleName}`, error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从目录加载所有 JSON 文件
|
||||
* @param moduleName 模块名称
|
||||
* @param dirPath 目录路径
|
||||
* @returns 加载的内容
|
||||
*/
|
||||
async loadFromDirectory(moduleName: string, dirPath: string): Promise<any[]> {
|
||||
try {
|
||||
const fullPath = path.resolve(dirPath);
|
||||
|
||||
try {
|
||||
await fs.access(fullPath);
|
||||
} catch (error) {
|
||||
this.logger.warn(`目录不存在: ${fullPath}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = await fs.readdir(fullPath);
|
||||
const jsonFiles = files.filter((file) => file.endsWith('.json'));
|
||||
const filePaths = jsonFiles.map((file) => path.join(fullPath, file));
|
||||
|
||||
return await this.mergeContentToArray(moduleName, filePaths);
|
||||
} catch (error) {
|
||||
this.logger.error(`从目录加载失败: ${moduleName}`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的模块
|
||||
* @param moduleName 模块名称
|
||||
* @returns 缓存的模块内容
|
||||
*/
|
||||
getCachedModule(moduleName: string): any {
|
||||
return this.moduleCache.get(moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除模块缓存
|
||||
* @param moduleName 模块名称,不传则清除所有
|
||||
*/
|
||||
clearCache(moduleName?: string): void {
|
||||
if (moduleName) {
|
||||
this.moduleCache.delete(moduleName);
|
||||
this.logger.log(`清除模块缓存: ${moduleName}`);
|
||||
} else {
|
||||
this.moduleCache.clear();
|
||||
this.logger.log('清除所有模块缓存');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载模块
|
||||
* @param moduleName 模块名称
|
||||
* @param filePath 文件路径
|
||||
* @returns 重新加载的内容
|
||||
*/
|
||||
async reloadModule(moduleName: string, filePath: string): Promise<any> {
|
||||
this.clearCache(moduleName);
|
||||
return await this.loadJsonModule(moduleName, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听文件变化并自动重新加载
|
||||
* @param moduleName 模块名称
|
||||
* @param filePath 文件路径
|
||||
* @param callback 变化回调
|
||||
*/
|
||||
watchModule(
|
||||
moduleName: string,
|
||||
filePath: string,
|
||||
callback: (content: any) => void,
|
||||
): void {
|
||||
const fullPath = path.resolve(filePath);
|
||||
|
||||
// 使用 fs.watch 替代 fs.watchFile
|
||||
const watcher = fsSync.watch(fullPath, (eventType) => {
|
||||
if (eventType === 'change') {
|
||||
this.logger.log(`文件变化,重新加载: ${moduleName}`);
|
||||
this.reloadModule(moduleName, filePath).then((content) => {
|
||||
callback(content);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.log(`开始监听文件变化: ${moduleName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止监听文件变化
|
||||
* @param filePath 文件路径
|
||||
*/
|
||||
unwatchModule(filePath: string): void {
|
||||
const fullPath = path.resolve(filePath);
|
||||
// fs.watch 返回的 watcher 需要手动关闭
|
||||
// 这里需要维护一个 watcher 映射来管理
|
||||
this.logger.log(`停止监听文件变化: ${filePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有缓存的模块名称
|
||||
* @returns 模块名称数组
|
||||
*/
|
||||
getCachedModuleNames(): string[] {
|
||||
return Array.from(this.moduleCache.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
* @returns 缓存统计信息
|
||||
*/
|
||||
getCacheStats(): {
|
||||
moduleCount: number;
|
||||
moduleNames: string[];
|
||||
totalSize: number;
|
||||
} {
|
||||
const moduleNames = this.getCachedModuleNames();
|
||||
let totalSize = 0;
|
||||
|
||||
for (const [name, content] of this.moduleCache) {
|
||||
totalSize += JSON.stringify(content).length;
|
||||
}
|
||||
|
||||
return {
|
||||
moduleCount: this.moduleCache.size,
|
||||
moduleNames,
|
||||
totalSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
357
src/common/logging/logging.interface.ts
Normal file
357
src/common/logging/logging.interface.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 日志接口定义
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 日志抽象
|
||||
*/
|
||||
export interface LoggingInterface {
|
||||
/**
|
||||
* 记录调试日志
|
||||
* @param message 日志消息
|
||||
* @param context 上下文
|
||||
* @param meta 元数据
|
||||
*/
|
||||
debug(message: string, context?: string, meta?: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* 记录信息日志
|
||||
* @param message 日志消息
|
||||
* @param context 上下文
|
||||
* @param meta 元数据
|
||||
*/
|
||||
info(message: string, context?: string, meta?: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* 记录警告日志
|
||||
* @param message 日志消息
|
||||
* @param context 上下文
|
||||
* @param meta 元数据
|
||||
*/
|
||||
warn(message: string, context?: string, meta?: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
* @param message 日志消息
|
||||
* @param context 上下文
|
||||
* @param meta 元数据
|
||||
*/
|
||||
error(message: string, context?: string, meta?: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* 记录致命错误日志
|
||||
* @param message 日志消息
|
||||
* @param context 上下文
|
||||
* @param meta 元数据
|
||||
*/
|
||||
fatal(message: string, context?: string, meta?: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
* @param level 日志级别
|
||||
* @param message 日志消息
|
||||
* @param context 上下文
|
||||
* @param meta 元数据
|
||||
*/
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: string,
|
||||
meta?: Record<string, any>,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 设置日志级别
|
||||
* @param level 日志级别
|
||||
*/
|
||||
setLevel(level: LogLevel): void;
|
||||
|
||||
/**
|
||||
* 获取当前日志级别
|
||||
* @returns 当前日志级别
|
||||
*/
|
||||
getLevel(): LogLevel;
|
||||
|
||||
/**
|
||||
* 创建子日志器
|
||||
* @param context 上下文
|
||||
* @returns 子日志器
|
||||
*/
|
||||
child(context: string): LoggingInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
export enum LogLevel {
|
||||
DEBUG = 'debug',
|
||||
INFO = 'info',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
/**
|
||||
* 结构化日志接口
|
||||
*/
|
||||
export interface StructuredLoggingInterface extends LoggingInterface {
|
||||
/**
|
||||
* 记录结构化日志
|
||||
* @param level 日志级别
|
||||
* @param message 日志消息
|
||||
* @param structuredData 结构化数据
|
||||
*/
|
||||
logStructured(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
structuredData: StructuredLogData,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 记录请求日志
|
||||
* @param request 请求数据
|
||||
* @param response 响应数据
|
||||
* @param duration 持续时间
|
||||
*/
|
||||
logRequest(
|
||||
request: RequestLogData,
|
||||
response: ResponseLogData,
|
||||
duration: number,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 记录数据库查询日志
|
||||
* @param query 查询数据
|
||||
* @param duration 持续时间
|
||||
* @param result 结果数据
|
||||
*/
|
||||
logDatabaseQuery(
|
||||
query: DatabaseQueryLogData,
|
||||
duration: number,
|
||||
result?: any,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 记录缓存操作日志
|
||||
* @param operation 操作数据
|
||||
* @param hit 是否命中
|
||||
* @param duration 持续时间
|
||||
*/
|
||||
logCacheOperation(
|
||||
operation: CacheOperationLogData,
|
||||
hit: boolean,
|
||||
duration: number,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 记录外部API调用日志
|
||||
* @param apiCall API调用数据
|
||||
* @param response 响应数据
|
||||
* @param duration 持续时间
|
||||
*/
|
||||
logExternalApiCall(
|
||||
apiCall: ExternalApiCallLogData,
|
||||
response: any,
|
||||
duration: number,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 记录业务事件日志
|
||||
* @param event 事件数据
|
||||
* @param user 用户数据
|
||||
* @param meta 元数据
|
||||
*/
|
||||
logBusinessEvent(
|
||||
event: BusinessEventLogData,
|
||||
user?: UserLogData,
|
||||
meta?: Record<string, any>,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结构化日志数据
|
||||
*/
|
||||
export interface StructuredLogData {
|
||||
timestamp: string;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
context?: string;
|
||||
traceId?: string;
|
||||
spanId?: string;
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
requestId?: string;
|
||||
correlationId?: string;
|
||||
meta?: Record<string, any>;
|
||||
error?: ErrorLogData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求日志数据
|
||||
*/
|
||||
export interface RequestLogData {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
body?: any;
|
||||
query?: Record<string, any>;
|
||||
params?: Record<string, any>;
|
||||
ip?: string;
|
||||
userAgent?: string;
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应日志数据
|
||||
*/
|
||||
export interface ResponseLogData {
|
||||
statusCode: number;
|
||||
headers: Record<string, string>;
|
||||
body?: any;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库查询日志数据
|
||||
*/
|
||||
export interface DatabaseQueryLogData {
|
||||
operation: string;
|
||||
table: string;
|
||||
query: string;
|
||||
params?: any[];
|
||||
connection?: string;
|
||||
transaction?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存操作日志数据
|
||||
*/
|
||||
export interface CacheOperationLogData {
|
||||
operation: string;
|
||||
key: string;
|
||||
ttl?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部API调用日志数据
|
||||
*/
|
||||
export interface ExternalApiCallLogData {
|
||||
service: string;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
body?: any;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务事件日志数据
|
||||
*/
|
||||
export interface BusinessEventLogData {
|
||||
event: string;
|
||||
action: string;
|
||||
resource: string;
|
||||
resourceId?: string;
|
||||
data?: Record<string, any>;
|
||||
result?: 'success' | 'failure' | 'partial';
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户日志数据
|
||||
*/
|
||||
export interface UserLogData {
|
||||
id: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误日志数据
|
||||
*/
|
||||
export interface ErrorLogData {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
code?: string;
|
||||
cause?: any;
|
||||
context?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志配置
|
||||
*/
|
||||
export interface LoggingConfig {
|
||||
level: LogLevel;
|
||||
format: 'json' | 'text' | 'simple';
|
||||
timestamp: boolean;
|
||||
colorize: boolean;
|
||||
prettyPrint: boolean;
|
||||
silent: boolean;
|
||||
exitOnError: boolean;
|
||||
transports: LogTransport[];
|
||||
defaultMeta?: Record<string, any>;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志传输器
|
||||
*/
|
||||
export interface LogTransport {
|
||||
type: 'console' | 'file' | 'http' | 'stream';
|
||||
level?: LogLevel;
|
||||
format?: string;
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志装饰器选项
|
||||
*/
|
||||
export interface LoggingOptions {
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
level?: LogLevel;
|
||||
|
||||
/**
|
||||
* 上下文
|
||||
*/
|
||||
context?: string;
|
||||
|
||||
/**
|
||||
* 是否记录参数
|
||||
*/
|
||||
logArgs?: boolean;
|
||||
|
||||
/**
|
||||
* 是否记录返回值
|
||||
*/
|
||||
logResult?: boolean;
|
||||
|
||||
/**
|
||||
* 是否记录执行时间
|
||||
*/
|
||||
logDuration?: boolean;
|
||||
|
||||
/**
|
||||
* 是否记录错误
|
||||
*/
|
||||
logError?: boolean;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* 自定义消息
|
||||
*/
|
||||
message?: string;
|
||||
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
126
src/common/logging/logging.module.ts
Normal file
126
src/common/logging/logging.module.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { LoggingService } from './logging.service';
|
||||
import {
|
||||
LoggingInterface,
|
||||
StructuredLoggingInterface,
|
||||
} from './logging.interface';
|
||||
|
||||
/**
|
||||
* 日志模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 日志配置
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: 'LOGGING_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 这里会根据配置选择具体的日志实现
|
||||
// 默认使用 Winston,也可以使用其他日志库
|
||||
const loggingType = configService.get('logging.type', 'winston');
|
||||
|
||||
if (loggingType === 'winston') {
|
||||
// 返回 Winston 日志实现
|
||||
return {
|
||||
async log(
|
||||
level: string,
|
||||
message: string,
|
||||
context?: any,
|
||||
): Promise<void> {
|
||||
console.log(`[${level.toUpperCase()}] ${message}`, context || '');
|
||||
},
|
||||
async error(message: string, context?: any): Promise<void> {
|
||||
console.error(`[ERROR] ${message}`, context || '');
|
||||
},
|
||||
async warn(message: string, context?: any): Promise<void> {
|
||||
console.warn(`[WARN] ${message}`, context || '');
|
||||
},
|
||||
async info(message: string, context?: any): Promise<void> {
|
||||
console.info(`[INFO] ${message}`, context || '');
|
||||
},
|
||||
async debug(message: string, context?: any): Promise<void> {
|
||||
console.debug(`[DEBUG] ${message}`, context || '');
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// 返回其他日志实现
|
||||
return {
|
||||
async log(
|
||||
level: string,
|
||||
message: string,
|
||||
context?: any,
|
||||
): Promise<void> {
|
||||
console.log(`[${level.toUpperCase()}] ${message}`, context || '');
|
||||
},
|
||||
async error(message: string, context?: any): Promise<void> {
|
||||
console.error(`[ERROR] ${message}`, context || '');
|
||||
},
|
||||
async warn(message: string, context?: any): Promise<void> {
|
||||
console.warn(`[WARN] ${message}`, context || '');
|
||||
},
|
||||
async info(message: string, context?: any): Promise<void> {
|
||||
console.info(`[INFO] ${message}`, context || '');
|
||||
},
|
||||
async debug(message: string, context?: any): Promise<void> {
|
||||
console.debug(`[DEBUG] ${message}`, context || '');
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: 'STRUCTURED_LOGGING_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 结构化日志实现
|
||||
return {
|
||||
async logStructured(
|
||||
level: string,
|
||||
message: string,
|
||||
metadata: any,
|
||||
): Promise<void> {
|
||||
const structuredLog = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
metadata,
|
||||
};
|
||||
console.log(JSON.stringify(structuredLog));
|
||||
},
|
||||
async logError(error: Error, context?: any): Promise<void> {
|
||||
const structuredLog = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'error',
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
context,
|
||||
};
|
||||
console.error(JSON.stringify(structuredLog));
|
||||
},
|
||||
async logPerformance(
|
||||
operation: string,
|
||||
duration: number,
|
||||
metadata?: any,
|
||||
): Promise<void> {
|
||||
const structuredLog = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'info',
|
||||
message: `Performance: ${operation}`,
|
||||
duration,
|
||||
metadata,
|
||||
};
|
||||
console.log(JSON.stringify(structuredLog));
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
LoggingService,
|
||||
],
|
||||
exports: [LoggingService],
|
||||
})
|
||||
export class LoggingModule {}
|
||||
542
src/common/logging/logging.service.ts
Normal file
542
src/common/logging/logging.service.ts
Normal file
@@ -0,0 +1,542 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import type {
|
||||
LoggingInterface,
|
||||
StructuredLoggingInterface,
|
||||
StructuredLogData,
|
||||
RequestLogData,
|
||||
ResponseLogData,
|
||||
DatabaseQueryLogData,
|
||||
CacheOperationLogData,
|
||||
ExternalApiCallLogData,
|
||||
BusinessEventLogData,
|
||||
UserLogData,
|
||||
ErrorLogData,
|
||||
LoggingOptions,
|
||||
} from './logging.interface';
|
||||
import { LogLevel } from './logging.interface';
|
||||
|
||||
/**
|
||||
* 日志服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 日志服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class LoggingService
|
||||
implements LoggingInterface, StructuredLoggingInterface
|
||||
{
|
||||
private readonly logger = new Logger(LoggingService.name);
|
||||
private currentLevel: LogLevel = LogLevel.INFO;
|
||||
|
||||
constructor(
|
||||
@Inject('LOGGING_PROVIDER')
|
||||
private readonly loggingProvider: LoggingInterface,
|
||||
@Inject('STRUCTURED_LOGGING_PROVIDER')
|
||||
private readonly structuredLoggingProvider: StructuredLoggingInterface,
|
||||
) {}
|
||||
|
||||
// ==================== 基础日志接口 ====================
|
||||
|
||||
/**
|
||||
* 记录调试日志
|
||||
*/
|
||||
debug(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
try {
|
||||
this.loggingProvider.debug(message, context, meta);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to log debug message: ${message}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录信息日志
|
||||
*/
|
||||
info(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
try {
|
||||
this.loggingProvider.info(message, context, meta);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to log info message: ${message}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录警告日志
|
||||
*/
|
||||
warn(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
try {
|
||||
this.loggingProvider.warn(message, context, meta);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to log warn message: ${message}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*/
|
||||
error(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
try {
|
||||
this.loggingProvider.error(message, context, meta);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to log error message: ${message}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录致命错误日志
|
||||
*/
|
||||
fatal(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
try {
|
||||
this.loggingProvider.fatal(message, context, meta);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to log fatal message: ${message}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*/
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: string,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
try {
|
||||
this.loggingProvider.log(level, message, context, meta);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to log message: ${message}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志级别
|
||||
*/
|
||||
setLevel(level: LogLevel): void {
|
||||
try {
|
||||
this.currentLevel = level;
|
||||
this.loggingProvider.setLevel(level);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set log level: ${level}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日志级别
|
||||
*/
|
||||
getLevel(): LogLevel {
|
||||
return this.currentLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建子日志器
|
||||
*/
|
||||
child(context: string): LoggingInterface {
|
||||
try {
|
||||
return this.loggingProvider.child(context);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to create child logger: ${context}`, error);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 结构化日志接口 ====================
|
||||
|
||||
/**
|
||||
* 记录结构化日志
|
||||
*/
|
||||
logStructured(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
structuredData: StructuredLogData,
|
||||
): void {
|
||||
try {
|
||||
this.structuredLoggingProvider.logStructured(
|
||||
level,
|
||||
message,
|
||||
structuredData,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to log structured message: ${message}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求日志
|
||||
*/
|
||||
logRequest(
|
||||
request: RequestLogData,
|
||||
response: ResponseLogData,
|
||||
duration: number,
|
||||
): void {
|
||||
try {
|
||||
this.structuredLoggingProvider.logRequest(request, response, duration);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to log request', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据库查询日志
|
||||
*/
|
||||
logDatabaseQuery(
|
||||
query: DatabaseQueryLogData,
|
||||
duration: number,
|
||||
result?: any,
|
||||
): void {
|
||||
try {
|
||||
this.structuredLoggingProvider.logDatabaseQuery(query, duration, result);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to log database query', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存操作日志
|
||||
*/
|
||||
logCacheOperation(
|
||||
operation: CacheOperationLogData,
|
||||
hit: boolean,
|
||||
duration: number,
|
||||
): void {
|
||||
try {
|
||||
this.structuredLoggingProvider.logCacheOperation(
|
||||
operation,
|
||||
hit,
|
||||
duration,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to log cache operation', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录外部API调用日志
|
||||
*/
|
||||
logExternalApiCall(
|
||||
apiCall: ExternalApiCallLogData,
|
||||
response: any,
|
||||
duration: number,
|
||||
): void {
|
||||
try {
|
||||
this.structuredLoggingProvider.logExternalApiCall(
|
||||
apiCall,
|
||||
response,
|
||||
duration,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to log external API call', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录业务事件日志
|
||||
*/
|
||||
logBusinessEvent(
|
||||
event: BusinessEventLogData,
|
||||
user?: UserLogData,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
try {
|
||||
this.structuredLoggingProvider.logBusinessEvent(event, user, meta);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to log business event', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 装饰器支持 ====================
|
||||
|
||||
/**
|
||||
* 日志装饰器实现
|
||||
*/
|
||||
async logMethod<T>(
|
||||
options: LoggingOptions,
|
||||
fn: () => T | Promise<T>,
|
||||
args: any[] = [],
|
||||
): Promise<T> {
|
||||
const {
|
||||
level = LogLevel.INFO,
|
||||
context = 'Method',
|
||||
logArgs = false,
|
||||
logResult = false,
|
||||
logDuration = true,
|
||||
logError = true,
|
||||
enabled = true,
|
||||
message,
|
||||
meta = {},
|
||||
} = options;
|
||||
|
||||
if (!enabled) {
|
||||
return await fn();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const logMessage = message || `${context} execution`;
|
||||
const logMeta = { ...meta };
|
||||
|
||||
try {
|
||||
// 记录方法开始
|
||||
if (logArgs) {
|
||||
logMeta.args = args;
|
||||
}
|
||||
|
||||
this.log(level, `${logMessage} started`, context, logMeta);
|
||||
|
||||
// 执行方法
|
||||
const result = await fn();
|
||||
|
||||
// 记录方法完成
|
||||
const duration = Date.now() - startTime;
|
||||
const successMeta = { ...logMeta };
|
||||
|
||||
if (logResult) {
|
||||
successMeta.result = result;
|
||||
}
|
||||
|
||||
if (logDuration) {
|
||||
successMeta.duration = duration;
|
||||
}
|
||||
|
||||
this.log(level, `${logMessage} completed`, context, successMeta);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
const errorMeta = { ...logMeta };
|
||||
|
||||
if (logDuration) {
|
||||
errorMeta.duration = duration;
|
||||
}
|
||||
|
||||
if (logError) {
|
||||
errorMeta.error = this.serializeError(error);
|
||||
}
|
||||
|
||||
this.error(`${logMessage} failed`, context, errorMeta);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 序列化错误
|
||||
*/
|
||||
private serializeError(error: any): ErrorLogData {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
code: (error as any).code,
|
||||
cause: (error as any).cause,
|
||||
context: (error as any).context,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'UnknownError',
|
||||
message: String(error),
|
||||
context: { originalError: error },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录HTTP请求
|
||||
*/
|
||||
logHttpRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
statusCode: number,
|
||||
duration: number,
|
||||
userAgent?: string,
|
||||
ip?: string,
|
||||
userId?: string,
|
||||
): void {
|
||||
const request: RequestLogData = {
|
||||
method,
|
||||
url,
|
||||
headers: {},
|
||||
ip,
|
||||
userAgent,
|
||||
userId,
|
||||
};
|
||||
|
||||
const response: ResponseLogData = {
|
||||
statusCode,
|
||||
headers: {},
|
||||
};
|
||||
|
||||
this.logRequest(request, response, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据库操作
|
||||
*/
|
||||
logDatabaseOperation(
|
||||
operation: string,
|
||||
table: string,
|
||||
query: string,
|
||||
duration: number,
|
||||
params?: any[],
|
||||
result?: any,
|
||||
): void {
|
||||
const queryData: DatabaseQueryLogData = {
|
||||
operation,
|
||||
table,
|
||||
query,
|
||||
params,
|
||||
};
|
||||
|
||||
this.logDatabaseQuery(queryData, duration, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存操作
|
||||
*/
|
||||
logCacheOperationSimple(
|
||||
operation: string,
|
||||
key: string,
|
||||
hit: boolean,
|
||||
duration: number,
|
||||
ttl?: number,
|
||||
size?: number,
|
||||
): void {
|
||||
const operationData: CacheOperationLogData = {
|
||||
operation,
|
||||
key,
|
||||
ttl,
|
||||
size,
|
||||
};
|
||||
|
||||
this.logCacheOperation(operationData, hit, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录外部API调用
|
||||
*/
|
||||
logExternalApiCallSimple(
|
||||
service: string,
|
||||
endpoint: string,
|
||||
method: string,
|
||||
statusCode: number,
|
||||
duration: number,
|
||||
headers?: Record<string, string>,
|
||||
body?: any,
|
||||
): void {
|
||||
const apiCall: ExternalApiCallLogData = {
|
||||
service,
|
||||
endpoint,
|
||||
method,
|
||||
headers: headers || {},
|
||||
body,
|
||||
};
|
||||
|
||||
const response = { statusCode };
|
||||
this.logExternalApiCall(apiCall, response, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录业务事件
|
||||
*/
|
||||
logBusinessEventSimple(
|
||||
event: string,
|
||||
action: string,
|
||||
resource: string,
|
||||
resourceId?: string,
|
||||
result?: 'success' | 'failure' | 'partial',
|
||||
data?: Record<string, any>,
|
||||
userId?: string,
|
||||
): void {
|
||||
const eventData: BusinessEventLogData = {
|
||||
event,
|
||||
action,
|
||||
resource,
|
||||
resourceId,
|
||||
result,
|
||||
data,
|
||||
};
|
||||
|
||||
const userData: UserLogData | undefined = userId
|
||||
? { id: userId }
|
||||
: undefined;
|
||||
this.logBusinessEvent(eventData, userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录性能指标
|
||||
*/
|
||||
logPerformance(
|
||||
operation: string,
|
||||
duration: number,
|
||||
context?: string,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
const performanceMeta = {
|
||||
operation,
|
||||
duration,
|
||||
...meta,
|
||||
};
|
||||
|
||||
if (duration > 1000) {
|
||||
this.warn(`Slow operation: ${operation}`, context, performanceMeta);
|
||||
} else if (duration > 500) {
|
||||
this.info(`Operation: ${operation}`, context, performanceMeta);
|
||||
} else {
|
||||
this.debug(`Operation: ${operation}`, context, performanceMeta);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录安全事件
|
||||
*/
|
||||
logSecurityEvent(
|
||||
event: string,
|
||||
severity: 'low' | 'medium' | 'high' | 'critical',
|
||||
context?: string,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
const securityMeta = {
|
||||
event,
|
||||
severity,
|
||||
...meta,
|
||||
};
|
||||
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
this.fatal(`Security event: ${event}`, context, securityMeta);
|
||||
break;
|
||||
case 'high':
|
||||
this.error(`Security event: ${event}`, context, securityMeta);
|
||||
break;
|
||||
case 'medium':
|
||||
this.warn(`Security event: ${event}`, context, securityMeta);
|
||||
break;
|
||||
case 'low':
|
||||
this.info(`Security event: ${event}`, context, securityMeta);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录审计日志
|
||||
*/
|
||||
logAudit(
|
||||
action: string,
|
||||
resource: string,
|
||||
resourceId?: string,
|
||||
userId?: string,
|
||||
result?: 'success' | 'failure',
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
const auditMeta = {
|
||||
action,
|
||||
resource,
|
||||
resourceId,
|
||||
userId,
|
||||
result,
|
||||
...meta,
|
||||
};
|
||||
|
||||
this.info(`Audit: ${action} on ${resource}`, 'audit', auditMeta);
|
||||
}
|
||||
}
|
||||
249
src/common/monitoring/monitoring.interface.ts
Normal file
249
src/common/monitoring/monitoring.interface.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 监控接口定义
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 监控抽象
|
||||
*/
|
||||
export interface MonitoringInterface {
|
||||
/**
|
||||
* 记录计数器指标
|
||||
* @param name 指标名称
|
||||
* @param value 计数值
|
||||
* @param labels 标签
|
||||
*/
|
||||
counter(name: string, value?: number, labels?: Record<string, string>): void;
|
||||
|
||||
/**
|
||||
* 记录直方图指标
|
||||
* @param name 指标名称
|
||||
* @param value 值
|
||||
* @param labels 标签
|
||||
*/
|
||||
histogram(name: string, value: number, labels?: Record<string, string>): void;
|
||||
|
||||
/**
|
||||
* 记录摘要指标
|
||||
* @param name 指标名称
|
||||
* @param value 值
|
||||
* @param labels 标签
|
||||
*/
|
||||
summary(name: string, value: number, labels?: Record<string, string>): void;
|
||||
|
||||
/**
|
||||
* 记录仪表盘指标
|
||||
* @param name 指标名称
|
||||
* @param value 值
|
||||
* @param labels 标签
|
||||
*/
|
||||
gauge(name: string, value: number, labels?: Record<string, string>): void;
|
||||
|
||||
/**
|
||||
* 开始计时
|
||||
* @param name 计时器名称
|
||||
* @param labels 标签
|
||||
* @returns 计时器对象
|
||||
*/
|
||||
startTimer(name: string, labels?: Record<string, string>): Timer;
|
||||
|
||||
/**
|
||||
* 记录执行时间
|
||||
* @param name 指标名称
|
||||
* @param fn 要执行的函数
|
||||
* @param labels 标签
|
||||
* @returns 函数执行结果
|
||||
*/
|
||||
time<T>(
|
||||
name: string,
|
||||
fn: () => T | Promise<T>,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<T>;
|
||||
|
||||
/**
|
||||
* 获取指标值
|
||||
* @param name 指标名称
|
||||
* @param labels 标签
|
||||
* @returns 指标值
|
||||
*/
|
||||
getMetric(
|
||||
name: string,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<number | null>;
|
||||
|
||||
/**
|
||||
* 获取所有指标
|
||||
* @returns 指标数据
|
||||
*/
|
||||
getMetrics(): Promise<string>;
|
||||
|
||||
/**
|
||||
* 重置指标
|
||||
* @param name 指标名称
|
||||
*/
|
||||
reset(name?: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计时器接口
|
||||
*/
|
||||
export interface Timer {
|
||||
/**
|
||||
* 结束计时
|
||||
* @param labels 标签
|
||||
*/
|
||||
end(labels?: Record<string, string>): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查接口
|
||||
*/
|
||||
export interface HealthCheckInterface {
|
||||
/**
|
||||
* 检查健康状态
|
||||
* @returns 健康状态
|
||||
*/
|
||||
check(): Promise<HealthStatus>;
|
||||
|
||||
/**
|
||||
* 注册健康检查
|
||||
* @param name 检查名称
|
||||
* @param checkFn 检查函数
|
||||
*/
|
||||
register(name: string, checkFn: () => Promise<boolean>): void;
|
||||
|
||||
/**
|
||||
* 取消注册健康检查
|
||||
* @param name 检查名称
|
||||
*/
|
||||
unregister(name: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康状态
|
||||
*/
|
||||
export interface HealthStatus {
|
||||
status: 'healthy' | 'unhealthy' | 'degraded';
|
||||
timestamp: number;
|
||||
uptime: number;
|
||||
checks: Record<string, HealthCheckResult>;
|
||||
version?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查结果
|
||||
*/
|
||||
export interface HealthCheckResult {
|
||||
status: 'pass' | 'fail' | 'warn';
|
||||
message?: string;
|
||||
timestamp: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能监控接口
|
||||
*/
|
||||
export interface PerformanceMonitoringInterface {
|
||||
/**
|
||||
* 记录请求
|
||||
* @param method HTTP方法
|
||||
* @param path 请求路径
|
||||
* @param statusCode 状态码
|
||||
* @param duration 持续时间
|
||||
* @param labels 标签
|
||||
*/
|
||||
recordRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
statusCode: number,
|
||||
duration: number,
|
||||
labels?: Record<string, string>,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 记录数据库查询
|
||||
* @param operation 操作类型
|
||||
* @param table 表名
|
||||
* @param duration 持续时间
|
||||
* @param labels 标签
|
||||
*/
|
||||
recordDatabaseQuery(
|
||||
operation: string,
|
||||
table: string,
|
||||
duration: number,
|
||||
labels?: Record<string, string>,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 记录缓存操作
|
||||
* @param operation 操作类型
|
||||
* @param key 缓存键
|
||||
* @param hit 是否命中
|
||||
* @param duration 持续时间
|
||||
* @param labels 标签
|
||||
*/
|
||||
recordCacheOperation(
|
||||
operation: string,
|
||||
key: string,
|
||||
hit: boolean,
|
||||
duration: number,
|
||||
labels?: Record<string, string>,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 记录外部API调用
|
||||
* @param service 服务名称
|
||||
* @param endpoint 端点
|
||||
* @param statusCode 状态码
|
||||
* @param duration 持续时间
|
||||
* @param labels 标签
|
||||
*/
|
||||
recordExternalApiCall(
|
||||
service: string,
|
||||
endpoint: string,
|
||||
statusCode: number,
|
||||
duration: number,
|
||||
labels?: Record<string, string>,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控配置
|
||||
*/
|
||||
export interface MonitoringConfig {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
path: string;
|
||||
defaultLabels?: Record<string, string>;
|
||||
collectDefaultMetrics?: boolean;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控装饰器选项
|
||||
*/
|
||||
export interface MonitoringOptions {
|
||||
/**
|
||||
* 指标名称
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* 指标类型
|
||||
*/
|
||||
type?: 'counter' | 'histogram' | 'summary' | 'gauge';
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
labels?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* 帮助文本
|
||||
*/
|
||||
help?: string;
|
||||
}
|
||||
161
src/common/monitoring/monitoring.module.ts
Normal file
161
src/common/monitoring/monitoring.module.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { MonitoringService } from './monitoring.service';
|
||||
import {
|
||||
MonitoringInterface,
|
||||
HealthCheckInterface,
|
||||
} from './monitoring.interface';
|
||||
|
||||
/**
|
||||
* 监控模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 监控配置
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: 'MONITORING_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 这里会根据配置选择具体的监控实现
|
||||
// 默认使用 Prometheus,也可以使用其他监控系统
|
||||
const monitoringType = configService.get(
|
||||
'monitoring.type',
|
||||
'prometheus',
|
||||
);
|
||||
|
||||
if (monitoringType === 'prometheus') {
|
||||
// 返回 Prometheus 监控实现
|
||||
return {
|
||||
async incrementCounter(
|
||||
name: string,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[MONITORING] Counter ${name} incremented`,
|
||||
labels || '',
|
||||
);
|
||||
},
|
||||
async setGauge(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[MONITORING] Gauge ${name} set to ${value}`,
|
||||
labels || '',
|
||||
);
|
||||
},
|
||||
async recordHistogram(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[MONITORING] Histogram ${name} recorded ${value}`,
|
||||
labels || '',
|
||||
);
|
||||
},
|
||||
async recordSummary(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[MONITORING] Summary ${name} recorded ${value}`,
|
||||
labels || '',
|
||||
);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// 返回其他监控实现
|
||||
return {
|
||||
async incrementCounter(
|
||||
name: string,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[MONITORING] Counter ${name} incremented`,
|
||||
labels || '',
|
||||
);
|
||||
},
|
||||
async setGauge(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[MONITORING] Gauge ${name} set to ${value}`,
|
||||
labels || '',
|
||||
);
|
||||
},
|
||||
async recordHistogram(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[MONITORING] Histogram ${name} recorded ${value}`,
|
||||
labels || '',
|
||||
);
|
||||
},
|
||||
async recordSummary(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[MONITORING] Summary ${name} recorded ${value}`,
|
||||
labels || '',
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: 'HEALTH_CHECK_PROVIDER',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// 健康检查实现
|
||||
return {
|
||||
async checkHealth(): Promise<{ status: string; details: any }> {
|
||||
return {
|
||||
status: 'healthy',
|
||||
details: {
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
version: process.version,
|
||||
},
|
||||
};
|
||||
},
|
||||
async checkDatabase(): Promise<{ status: string; details: any }> {
|
||||
return {
|
||||
status: 'healthy',
|
||||
details: {
|
||||
timestamp: new Date().toISOString(),
|
||||
message: 'Database connection healthy',
|
||||
},
|
||||
};
|
||||
},
|
||||
async checkRedis(): Promise<{ status: string; details: any }> {
|
||||
return {
|
||||
status: 'healthy',
|
||||
details: {
|
||||
timestamp: new Date().toISOString(),
|
||||
message: 'Redis connection healthy',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
MonitoringService,
|
||||
],
|
||||
exports: [MonitoringService],
|
||||
})
|
||||
export class MonitoringModule {}
|
||||
462
src/common/monitoring/monitoring.service.ts
Normal file
462
src/common/monitoring/monitoring.service.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import type {
|
||||
MonitoringInterface,
|
||||
HealthCheckInterface,
|
||||
PerformanceMonitoringInterface,
|
||||
Timer,
|
||||
HealthStatus,
|
||||
HealthCheckResult,
|
||||
MonitoringOptions,
|
||||
} from './monitoring.interface';
|
||||
|
||||
/**
|
||||
* 监控服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 监控服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class MonitoringService
|
||||
implements
|
||||
MonitoringInterface,
|
||||
HealthCheckInterface,
|
||||
PerformanceMonitoringInterface
|
||||
{
|
||||
private readonly logger = new Logger(MonitoringService.name);
|
||||
private healthChecks = new Map<string, () => Promise<boolean>>();
|
||||
private startTime = Date.now();
|
||||
|
||||
constructor(
|
||||
@Inject('MONITORING_PROVIDER')
|
||||
private readonly monitoringProvider: MonitoringInterface,
|
||||
@Inject('HEALTH_CHECK_PROVIDER')
|
||||
private readonly healthCheckProvider: HealthCheckInterface,
|
||||
) {}
|
||||
|
||||
// ==================== 基础监控接口 ====================
|
||||
|
||||
/**
|
||||
* 记录计数器指标
|
||||
*/
|
||||
counter(
|
||||
name: string,
|
||||
value: number = 1,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
try {
|
||||
this.monitoringProvider.counter(name, value, labels);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record counter: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录直方图指标
|
||||
*/
|
||||
histogram(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
try {
|
||||
this.monitoringProvider.histogram(name, value, labels);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record histogram: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录摘要指标
|
||||
*/
|
||||
summary(name: string, value: number, labels?: Record<string, string>): void {
|
||||
try {
|
||||
this.monitoringProvider.summary(name, value, labels);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record summary: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录仪表盘指标
|
||||
*/
|
||||
gauge(name: string, value: number, labels?: Record<string, string>): void {
|
||||
try {
|
||||
this.monitoringProvider.gauge(name, value, labels);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record gauge: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始计时
|
||||
*/
|
||||
startTimer(name: string, labels?: Record<string, string>): Timer {
|
||||
try {
|
||||
return this.monitoringProvider.startTimer(name, labels);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to start timer: ${name}`, error);
|
||||
return {
|
||||
end: () => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录执行时间
|
||||
*/
|
||||
async time<T>(
|
||||
name: string,
|
||||
fn: () => T | Promise<T>,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<T> {
|
||||
const timer = this.startTimer(name, labels);
|
||||
try {
|
||||
const result = await fn();
|
||||
return result;
|
||||
} finally {
|
||||
timer.end(labels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标值
|
||||
*/
|
||||
async getMetric(
|
||||
name: string,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
return this.monitoringProvider.getMetric(name, labels);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get metric: ${name}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有指标
|
||||
*/
|
||||
async getMetrics(): Promise<string> {
|
||||
try {
|
||||
return this.monitoringProvider.getMetrics();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get metrics', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指标
|
||||
*/
|
||||
reset(name?: string): void {
|
||||
try {
|
||||
this.monitoringProvider.reset(name);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to reset metrics: ${name || 'all'}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 健康检查接口 ====================
|
||||
|
||||
/**
|
||||
* 检查健康状态
|
||||
*/
|
||||
async check(): Promise<HealthStatus> {
|
||||
const checks: Record<string, HealthCheckResult> = {};
|
||||
let overallStatus: 'healthy' | 'unhealthy' | 'degraded' = 'healthy';
|
||||
const hasWarnings = false;
|
||||
|
||||
for (const [name, checkFn] of this.healthChecks) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const isHealthy = await checkFn();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
checks[name] = {
|
||||
status: isHealthy ? 'pass' : 'fail',
|
||||
timestamp: Date.now(),
|
||||
duration,
|
||||
};
|
||||
|
||||
if (!isHealthy) {
|
||||
overallStatus = 'unhealthy';
|
||||
}
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
checks[name] = {
|
||||
status: 'fail',
|
||||
message: error.message,
|
||||
timestamp: Date.now(),
|
||||
duration,
|
||||
};
|
||||
overallStatus = 'unhealthy';
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有健康检查,默认为健康
|
||||
if (Object.keys(checks).length === 0) {
|
||||
checks['default'] = {
|
||||
status: 'pass',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: overallStatus,
|
||||
timestamp: Date.now(),
|
||||
uptime: Date.now() - this.startTime,
|
||||
checks,
|
||||
version: process.env.npm_package_version,
|
||||
environment: process.env.NODE_ENV,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册健康检查
|
||||
*/
|
||||
register(name: string, checkFn: () => Promise<boolean>): void {
|
||||
this.healthChecks.set(name, checkFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册健康检查
|
||||
*/
|
||||
unregister(name: string): void {
|
||||
this.healthChecks.delete(name);
|
||||
}
|
||||
|
||||
// ==================== 性能监控接口 ====================
|
||||
|
||||
/**
|
||||
* 记录请求
|
||||
*/
|
||||
recordRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
statusCode: number,
|
||||
duration: number,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
const requestLabels = {
|
||||
method,
|
||||
path,
|
||||
status_code: statusCode.toString(),
|
||||
...labels,
|
||||
};
|
||||
|
||||
// 记录请求计数
|
||||
this.counter('http_requests_total', 1, requestLabels);
|
||||
|
||||
// 记录请求持续时间
|
||||
this.histogram(
|
||||
'http_request_duration_seconds',
|
||||
duration / 1000,
|
||||
requestLabels,
|
||||
);
|
||||
|
||||
// 记录状态码分布
|
||||
this.counter('http_status_codes_total', 1, {
|
||||
status_code: statusCode.toString(),
|
||||
...labels,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据库查询
|
||||
*/
|
||||
recordDatabaseQuery(
|
||||
operation: string,
|
||||
table: string,
|
||||
duration: number,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
const dbLabels = {
|
||||
operation,
|
||||
table,
|
||||
...labels,
|
||||
};
|
||||
|
||||
// 记录查询计数
|
||||
this.counter('database_queries_total', 1, dbLabels);
|
||||
|
||||
// 记录查询持续时间
|
||||
this.histogram(
|
||||
'database_query_duration_seconds',
|
||||
duration / 1000,
|
||||
dbLabels,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存操作
|
||||
*/
|
||||
recordCacheOperation(
|
||||
operation: string,
|
||||
key: string,
|
||||
hit: boolean,
|
||||
duration: number,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
const cacheLabels = {
|
||||
operation,
|
||||
hit: hit.toString(),
|
||||
...labels,
|
||||
};
|
||||
|
||||
// 记录缓存操作计数
|
||||
this.counter('cache_operations_total', 1, cacheLabels);
|
||||
|
||||
// 记录缓存命中率
|
||||
this.counter('cache_hits_total', hit ? 1 : 0, cacheLabels);
|
||||
this.counter('cache_misses_total', hit ? 0 : 1, cacheLabels);
|
||||
|
||||
// 记录缓存操作持续时间
|
||||
this.histogram(
|
||||
'cache_operation_duration_seconds',
|
||||
duration / 1000,
|
||||
cacheLabels,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录外部API调用
|
||||
*/
|
||||
recordExternalApiCall(
|
||||
service: string,
|
||||
endpoint: string,
|
||||
statusCode: number,
|
||||
duration: number,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
const apiLabels = {
|
||||
service,
|
||||
endpoint,
|
||||
status_code: statusCode.toString(),
|
||||
...labels,
|
||||
};
|
||||
|
||||
// 记录API调用计数
|
||||
this.counter('external_api_calls_total', 1, apiLabels);
|
||||
|
||||
// 记录API调用持续时间
|
||||
this.histogram(
|
||||
'external_api_call_duration_seconds',
|
||||
duration / 1000,
|
||||
apiLabels,
|
||||
);
|
||||
|
||||
// 记录API状态码分布
|
||||
this.counter('external_api_status_codes_total', 1, {
|
||||
service,
|
||||
status_code: statusCode.toString(),
|
||||
...labels,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 装饰器支持 ====================
|
||||
|
||||
/**
|
||||
* 监控装饰器实现
|
||||
*/
|
||||
async monitor<T>(
|
||||
options: MonitoringOptions,
|
||||
fn: () => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
const { name, type = 'histogram', labels, enabled = true } = options;
|
||||
|
||||
if (!enabled || !name) {
|
||||
return await fn();
|
||||
}
|
||||
|
||||
const timer = this.startTimer(name, labels);
|
||||
try {
|
||||
const result = await fn();
|
||||
return result;
|
||||
} finally {
|
||||
const duration = timer.end(labels);
|
||||
this.histogram(name, duration / 1000, labels);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 记录错误
|
||||
*/
|
||||
recordError(error: Error, labels?: Record<string, string>): void {
|
||||
this.counter('errors_total', 1, {
|
||||
error_type: error.constructor.name,
|
||||
error_message: error.message,
|
||||
...labels,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录内存使用
|
||||
*/
|
||||
recordMemoryUsage(): void {
|
||||
const usage = process.memoryUsage();
|
||||
|
||||
this.gauge('memory_usage_bytes', usage.heapUsed, { type: 'heap_used' });
|
||||
this.gauge('memory_usage_bytes', usage.heapTotal, { type: 'heap_total' });
|
||||
this.gauge('memory_usage_bytes', usage.external, { type: 'external' });
|
||||
this.gauge('memory_usage_bytes', usage.rss, { type: 'rss' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录CPU使用
|
||||
*/
|
||||
recordCpuUsage(): void {
|
||||
const usage = process.cpuUsage();
|
||||
const total = usage.user + usage.system;
|
||||
|
||||
this.gauge('cpu_usage_microseconds', total, { type: 'total' });
|
||||
this.gauge('cpu_usage_microseconds', usage.user, { type: 'user' });
|
||||
this.gauge('cpu_usage_microseconds', usage.system, { type: 'system' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录事件循环延迟
|
||||
*/
|
||||
recordEventLoopDelay(): void {
|
||||
const start = process.hrtime.bigint();
|
||||
setImmediate(() => {
|
||||
const delay = Number(process.hrtime.bigint() - start) / 1000000; // 转换为毫秒
|
||||
this.histogram('event_loop_delay_milliseconds', delay);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录垃圾回收
|
||||
*/
|
||||
recordGarbageCollection(): void {
|
||||
if (global.gc) {
|
||||
const start = process.hrtime.bigint();
|
||||
global.gc();
|
||||
const duration = Number(process.hrtime.bigint() - start) / 1000000; // 转换为毫秒
|
||||
this.histogram('gc_duration_milliseconds', duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动默认指标收集
|
||||
*/
|
||||
startDefaultMetrics(): void {
|
||||
// 记录内存使用
|
||||
setInterval(() => {
|
||||
this.recordMemoryUsage();
|
||||
}, 10000); // 每10秒
|
||||
|
||||
// 记录CPU使用
|
||||
setInterval(() => {
|
||||
this.recordCpuUsage();
|
||||
}, 10000); // 每10秒
|
||||
|
||||
// 记录事件循环延迟
|
||||
setInterval(() => {
|
||||
this.recordEventLoopDelay();
|
||||
}, 5000); // 每5秒
|
||||
|
||||
this.logger.log('Default metrics collection started');
|
||||
}
|
||||
}
|
||||
69
src/common/pipes/parse-diy-form.pipe.ts
Normal file
69
src/common/pipes/parse-diy-form.pipe.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
PipeTransform,
|
||||
Injectable,
|
||||
ArgumentMetadata,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 自定义表单数据解析管道
|
||||
* 基于 Java 框架中的 DiyFormDriver 转换逻辑
|
||||
* 对应 Java: DiyFormDriver.convert()
|
||||
*/
|
||||
@Injectable()
|
||||
export class ParseDiyFormPipe implements PipeTransform<any, any> {
|
||||
transform(value: any, metadata: ArgumentMetadata): any {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 根据组件类型进行数据转换
|
||||
const componentType = value.componentType;
|
||||
const fieldValue = value.fieldValue;
|
||||
|
||||
switch (componentType) {
|
||||
case 'FormRadio':
|
||||
case 'FormCheckbox':
|
||||
// 单选和多选需要解析为数组
|
||||
if (typeof fieldValue === 'string') {
|
||||
try {
|
||||
return JSON.parse(fieldValue);
|
||||
} catch {
|
||||
return [fieldValue];
|
||||
}
|
||||
}
|
||||
return fieldValue;
|
||||
|
||||
case 'FormDate':
|
||||
// 日期直接返回
|
||||
return fieldValue;
|
||||
|
||||
case 'FormDateScope':
|
||||
case 'FormTimeScope':
|
||||
// 日期范围和时间范围需要解析为对象
|
||||
if (typeof fieldValue === 'string') {
|
||||
try {
|
||||
return JSON.parse(fieldValue);
|
||||
} catch {
|
||||
throw new BadRequestException('日期范围格式错误');
|
||||
}
|
||||
}
|
||||
return fieldValue;
|
||||
|
||||
case 'FormImage':
|
||||
// 图片需要解析为数组
|
||||
if (typeof fieldValue === 'string') {
|
||||
try {
|
||||
return JSON.parse(fieldValue);
|
||||
} catch {
|
||||
return [fieldValue];
|
||||
}
|
||||
}
|
||||
return fieldValue;
|
||||
|
||||
default:
|
||||
// 其他类型直接返回
|
||||
return fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/common/pipes/pipes.module.ts
Normal file
79
src/common/pipes/pipes.module.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import {
|
||||
ValidationPipe,
|
||||
ParseIntPipe,
|
||||
ParseBoolPipe,
|
||||
ParseArrayPipe,
|
||||
ParseUUIDPipe,
|
||||
DefaultValuePipe,
|
||||
} from '@nestjs/common';
|
||||
import { ParseDiyFormPipe } from './parse-diy-form.pipe';
|
||||
|
||||
/**
|
||||
* 管道模块 - 基础设施层
|
||||
* 基于 NestJS 官方内置管道
|
||||
* 参考: https://docs.nestjs.cn/pipes
|
||||
* 对应 Java: @InitBinder + @Valid
|
||||
*
|
||||
* 统一管理所有验证管道,避免重复定义
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
// 统一的验证管道配置
|
||||
{
|
||||
provide: 'VALIDATION_PIPE',
|
||||
useValue: new ValidationPipe({
|
||||
whitelist: true, // 自动过滤掉没有装饰器的属性
|
||||
forbidNonWhitelisted: true, // 如果有非白名单属性,抛出错误
|
||||
transform: true, // 自动转换类型
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true, // 启用隐式转换
|
||||
},
|
||||
validateCustomDecorators: true, // 验证自定义装饰器
|
||||
exceptionFactory: (errors) => {
|
||||
// 自定义错误格式
|
||||
const result = errors.map((error) => ({
|
||||
property: error.property,
|
||||
value: error.value,
|
||||
constraints: error.constraints,
|
||||
}));
|
||||
return new Error(JSON.stringify(result));
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: 'PARSE_INT_PIPE',
|
||||
useValue: new ParseIntPipe(),
|
||||
},
|
||||
{
|
||||
provide: 'PARSE_BOOL_PIPE',
|
||||
useValue: new ParseBoolPipe(),
|
||||
},
|
||||
{
|
||||
provide: 'PARSE_ARRAY_PIPE',
|
||||
useValue: new ParseArrayPipe({ items: String, separator: ',' }),
|
||||
},
|
||||
{
|
||||
provide: 'PARSE_UUID_PIPE',
|
||||
useValue: new ParseUUIDPipe(),
|
||||
},
|
||||
{
|
||||
provide: 'DEFAULT_VALUE_PIPE',
|
||||
useValue: new DefaultValuePipe(''),
|
||||
},
|
||||
// 业务特定的自定义管道
|
||||
ParseDiyFormPipe,
|
||||
],
|
||||
exports: [
|
||||
'VALIDATION_PIPE',
|
||||
'PARSE_INT_PIPE',
|
||||
'PARSE_BOOL_PIPE',
|
||||
'PARSE_ARRAY_PIPE',
|
||||
'PARSE_UUID_PIPE',
|
||||
'DEFAULT_VALUE_PIPE',
|
||||
// 业务特定的自定义管道
|
||||
ParseDiyFormPipe,
|
||||
],
|
||||
})
|
||||
export class PipesModule {}
|
||||
14
src/common/plugins/captcha/captcha.module.ts
Normal file
14
src/common/plugins/captcha/captcha.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CaptchaService } from './captcha.service';
|
||||
|
||||
/**
|
||||
* 验证码插件模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: CaptchaUtils
|
||||
*/
|
||||
@Module({
|
||||
providers: [CaptchaService],
|
||||
exports: [CaptchaService],
|
||||
})
|
||||
export class CaptchaModule {}
|
||||
69
src/common/plugins/captcha/captcha.service.ts
Normal file
69
src/common/plugins/captcha/captcha.service.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as svgCaptcha from 'svg-captcha';
|
||||
|
||||
/**
|
||||
* 验证码服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: CaptchaUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class CaptchaService {
|
||||
/**
|
||||
* 生成图片验证码
|
||||
*/
|
||||
generate(options?: svgCaptcha.ConfigObject): svgCaptcha.CaptchaObj {
|
||||
return svgCaptcha.create(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成数学验证码
|
||||
*/
|
||||
generateMath(options?: svgCaptcha.ConfigObject): svgCaptcha.CaptchaObj {
|
||||
return svgCaptcha.createMathExpr(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成简单验证码
|
||||
*/
|
||||
generateSimple(options?: svgCaptcha.ConfigObject): svgCaptcha.CaptchaObj {
|
||||
return svgCaptcha.create({
|
||||
size: 4,
|
||||
ignoreChars: '0o1il',
|
||||
noise: 1,
|
||||
color: true,
|
||||
background: '#f0f0f0',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成复杂验证码
|
||||
*/
|
||||
generateComplex(options?: svgCaptcha.ConfigObject): svgCaptcha.CaptchaObj {
|
||||
return svgCaptcha.create({
|
||||
size: 6,
|
||||
noise: 3,
|
||||
color: true,
|
||||
background: '#f0f0f0',
|
||||
fontSize: 50,
|
||||
width: 150,
|
||||
height: 50,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
*/
|
||||
verify(
|
||||
input: string,
|
||||
captcha: string,
|
||||
caseSensitive: boolean = false,
|
||||
): boolean {
|
||||
if (caseSensitive) {
|
||||
return input === captcha;
|
||||
}
|
||||
return input.toLowerCase() === captcha.toLowerCase();
|
||||
}
|
||||
}
|
||||
4
src/common/plugins/index.ts
Normal file
4
src/common/plugins/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './plugins.module';
|
||||
export * from './captcha/captcha.service';
|
||||
export * from './qrcode/qrcode.service';
|
||||
export * from './wechat/wechat.service';
|
||||
17
src/common/plugins/plugins.module.ts
Normal file
17
src/common/plugins/plugins.module.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CaptchaModule } from './captcha/captcha.module';
|
||||
import { QrcodeModule } from './qrcode/qrcode.module';
|
||||
import { WechatModule } from './wechat/wechat.module';
|
||||
|
||||
/**
|
||||
* 基础功能插件模块 - Common层
|
||||
* 基于 NestJS 实现
|
||||
* 对应 Java: 基础功能插件
|
||||
*
|
||||
* 包含所有基础功能插件,作为框架基础能力
|
||||
*/
|
||||
@Module({
|
||||
imports: [CaptchaModule, QrcodeModule, WechatModule],
|
||||
exports: [CaptchaModule, QrcodeModule, WechatModule],
|
||||
})
|
||||
export class PluginsModule {}
|
||||
14
src/common/plugins/qrcode/qrcode.module.ts
Normal file
14
src/common/plugins/qrcode/qrcode.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { QrcodeService } from './qrcode.service';
|
||||
|
||||
/**
|
||||
* 二维码插件模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: QRCodeUtils
|
||||
*/
|
||||
@Module({
|
||||
providers: [QrcodeService],
|
||||
exports: [QrcodeService],
|
||||
})
|
||||
export class QrcodeModule {}
|
||||
144
src/common/plugins/qrcode/qrcode.service.ts
Normal file
144
src/common/plugins/qrcode/qrcode.service.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as QRCode from 'qrcode';
|
||||
|
||||
/**
|
||||
* 二维码服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: QRCodeUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class QrcodeService {
|
||||
/**
|
||||
* 生成二维码字符串
|
||||
*/
|
||||
async toString(
|
||||
text: string,
|
||||
options?: QRCode.QRCodeToStringOptions,
|
||||
): Promise<string> {
|
||||
return QRCode.toString(text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码数据URL
|
||||
*/
|
||||
async toDataURL(
|
||||
text: string,
|
||||
options?: QRCode.QRCodeToDataURLOptions,
|
||||
): Promise<string> {
|
||||
return QRCode.toDataURL(text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码Buffer
|
||||
*/
|
||||
async toBuffer(
|
||||
text: string,
|
||||
options?: QRCode.QRCodeToBufferOptions,
|
||||
): Promise<Buffer> {
|
||||
return QRCode.toBuffer(text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码文件
|
||||
*/
|
||||
async toFile(
|
||||
path: string,
|
||||
text: string,
|
||||
options?: QRCode.QRCodeToFileOptions,
|
||||
): Promise<void> {
|
||||
return QRCode.toFile(path, text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成简单二维码
|
||||
*/
|
||||
async generateSimple(text: string): Promise<string> {
|
||||
return this.toDataURL(text, {
|
||||
type: 'image/png',
|
||||
// quality: 0.92, // 新版本 qrcode 不支持 quality 参数
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成彩色二维码
|
||||
*/
|
||||
async generateColor(
|
||||
text: string,
|
||||
darkColor: string = '#000000',
|
||||
lightColor: string = '#FFFFFF',
|
||||
): Promise<string> {
|
||||
return this.toDataURL(text, {
|
||||
type: 'image/png',
|
||||
// quality: 0.92, // 新版本 qrcode 不支持 quality 参数
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: darkColor,
|
||||
light: lightColor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成大尺寸二维码
|
||||
*/
|
||||
async generateLarge(text: string, size: number = 300): Promise<string> {
|
||||
return this.toDataURL(text, {
|
||||
type: 'image/png',
|
||||
// quality: 0.92, // 新版本 qrcode 不支持 quality 参数
|
||||
margin: 1,
|
||||
width: size,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成小尺寸二维码
|
||||
*/
|
||||
async generateSmall(text: string, size: number = 100): Promise<string> {
|
||||
return this.toDataURL(text, {
|
||||
type: 'image/png',
|
||||
// quality: 0.92, // 新版本 qrcode 不支持 quality 参数
|
||||
margin: 1,
|
||||
width: size,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成SVG二维码
|
||||
*/
|
||||
async generateSVG(
|
||||
text: string,
|
||||
options?: QRCode.QRCodeToStringOptions,
|
||||
): Promise<string> {
|
||||
return this.toString(text, {
|
||||
type: 'svg',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成ASCII二维码
|
||||
*/
|
||||
async generateASCII(
|
||||
text: string,
|
||||
options?: QRCode.QRCodeToStringOptions,
|
||||
): Promise<string> {
|
||||
return this.toString(text, {
|
||||
type: 'terminal',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
14
src/common/plugins/wechat/wechat.module.ts
Normal file
14
src/common/plugins/wechat/wechat.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WechatService } from './wechat.service';
|
||||
|
||||
/**
|
||||
* 微信插件模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: WechatUtils
|
||||
*/
|
||||
@Module({
|
||||
providers: [WechatService],
|
||||
exports: [WechatService],
|
||||
})
|
||||
export class WechatModule {}
|
||||
232
src/common/plugins/wechat/wechat.service.ts
Normal file
232
src/common/plugins/wechat/wechat.service.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* 微信服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: WechatUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class WechatService {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 验证微信签名
|
||||
*/
|
||||
verifySignature(
|
||||
signature: string,
|
||||
timestamp: string,
|
||||
nonce: string,
|
||||
token: string,
|
||||
): boolean {
|
||||
const tmpArr = [token, timestamp, nonce].sort();
|
||||
const tmpStr = tmpArr.join('');
|
||||
const hash = crypto.createHash('sha1').update(tmpStr).digest('hex');
|
||||
return hash === signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信签名
|
||||
*/
|
||||
generateSignature(params: Record<string, any>, key: string): string {
|
||||
// 按参数名排序
|
||||
const sortedParams = Object.keys(params)
|
||||
.sort()
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
const stringSignTemp = `${sortedParams}&key=${key}`;
|
||||
return crypto
|
||||
.createHash('md5')
|
||||
.update(stringSignTemp)
|
||||
.digest('hex')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
generateNonceStr(length: number = 32): string {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成时间戳
|
||||
*/
|
||||
generateTimestamp(): string {
|
||||
return Math.floor(Date.now() / 1000).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信支付签名
|
||||
*/
|
||||
generatePaySignature(params: Record<string, any>, key: string): string {
|
||||
// 过滤空值
|
||||
const filteredParams = Object.keys(params)
|
||||
.filter(
|
||||
(key) =>
|
||||
params[key] !== '' &&
|
||||
params[key] !== null &&
|
||||
params[key] !== undefined,
|
||||
)
|
||||
.reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = params[key];
|
||||
return obj;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
// 按参数名排序
|
||||
const sortedParams = Object.keys(filteredParams)
|
||||
.sort()
|
||||
.map((key) => `${key}=${filteredParams[key]}`)
|
||||
.join('&');
|
||||
|
||||
const stringSignTemp = `${sortedParams}&key=${key}`;
|
||||
return crypto
|
||||
.createHash('md5')
|
||||
.update(stringSignTemp)
|
||||
.digest('hex')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证微信支付签名
|
||||
*/
|
||||
verifyPaySignature(
|
||||
params: Record<string, any>,
|
||||
signature: string,
|
||||
key: string,
|
||||
): boolean {
|
||||
const expectedSignature = this.generatePaySignature(params, key);
|
||||
return expectedSignature === signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信JSAPI签名
|
||||
*/
|
||||
generateJSAPISignature(params: Record<string, any>, key: string): string {
|
||||
const sortedParams = Object.keys(params)
|
||||
.sort()
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
return crypto.createHash('sha1').update(sortedParams).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信小程序签名
|
||||
*/
|
||||
generateMiniProgramSignature(
|
||||
params: Record<string, any>,
|
||||
key: string,
|
||||
): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信H5签名
|
||||
*/
|
||||
generateH5Signature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信APP签名
|
||||
*/
|
||||
generateAPPSignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信Native签名
|
||||
*/
|
||||
generateNativeSignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信扫码签名
|
||||
*/
|
||||
generateScanSignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信刷卡签名
|
||||
*/
|
||||
generateMicropaySignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信退款签名
|
||||
*/
|
||||
generateRefundSignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信查询签名
|
||||
*/
|
||||
generateQuerySignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信关闭订单签名
|
||||
*/
|
||||
generateCloseSignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信撤销签名
|
||||
*/
|
||||
generateReverseSignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信转换短链接签名
|
||||
*/
|
||||
generateShortUrlSignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信转换长链接签名
|
||||
*/
|
||||
generateLongUrlSignature(params: Record<string, any>, key: string): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信下载对账单签名
|
||||
*/
|
||||
generateDownloadBillSignature(
|
||||
params: Record<string, any>,
|
||||
key: string,
|
||||
): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信拉取订单评价数据签名
|
||||
*/
|
||||
generateBatchQueryCommentSignature(
|
||||
params: Record<string, any>,
|
||||
key: string,
|
||||
): string {
|
||||
return this.generatePaySignature(params, key);
|
||||
}
|
||||
}
|
||||
42
src/common/queue/queue.module.ts
Normal file
42
src/common/queue/queue.module.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* 队列模块 - 基础设施层
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/techniques/queues
|
||||
* 使用 BullMQ 作为队列引擎
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
BullModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const redisConfig = configService.get('redis');
|
||||
|
||||
return {
|
||||
connection: {
|
||||
host: redisConfig.host,
|
||||
port: redisConfig.port,
|
||||
password: redisConfig.password,
|
||||
db: redisConfig.db,
|
||||
},
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 5,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 2000,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
exports: [BullModule],
|
||||
})
|
||||
export class QueueModule {}
|
||||
106
src/common/response/page-result.class.ts
Normal file
106
src/common/response/page-result.class.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 分页结果类
|
||||
* 与Java PageResult格式完全一致
|
||||
*
|
||||
* 格式:
|
||||
* {
|
||||
* "currentPage": 1, // 当前页
|
||||
* "perPage": 15, // 每页大小
|
||||
* "total": 100, // 总记录数
|
||||
* "data": [] // 数据列表
|
||||
* }
|
||||
*/
|
||||
export class PageResult<T> {
|
||||
/**
|
||||
* 当前请求页
|
||||
*/
|
||||
currentPage: number;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
perPage: number;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* 记录结果
|
||||
*/
|
||||
data: T[];
|
||||
|
||||
constructor(page: number, limit: number, total: number = 0, data: T[] = []) {
|
||||
this.currentPage = page;
|
||||
this.perPage = limit;
|
||||
this.total = total;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分页结果
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @returns PageResult<T>
|
||||
*/
|
||||
static build<T>(page: number, limit: number): PageResult<T> {
|
||||
return new PageResult<T>(page, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分页结果
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @param total 总记录数
|
||||
* @returns PageResult<T>
|
||||
*/
|
||||
static buildWithTotal<T>(page: number, limit: number, total: number): PageResult<T> {
|
||||
return new PageResult<T>(page, limit, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分页结果
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @param total 总记录数
|
||||
* @param data 数据列表
|
||||
* @returns PageResult<T>
|
||||
*/
|
||||
static buildWithData<T>(page: number, limit: number, total: number, data: T[]): PageResult<T> {
|
||||
return new PageResult<T>(page, limit, total, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置数据
|
||||
* @param data 数据列表
|
||||
* @returns PageResult<T>
|
||||
*/
|
||||
setData(data: T[]): PageResult<T> {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置总数
|
||||
* @param total 总记录数
|
||||
* @returns PageResult<T>
|
||||
*/
|
||||
setTotal(total: number): PageResult<T> {
|
||||
this.total = total;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为JSON格式
|
||||
* @returns object
|
||||
*/
|
||||
toJSON(): object {
|
||||
return {
|
||||
currentPage: this.currentPage,
|
||||
perPage: this.perPage,
|
||||
total: this.total,
|
||||
data: this.data,
|
||||
};
|
||||
}
|
||||
}
|
||||
42
src/common/response/response.decorator.ts
Normal file
42
src/common/response/response.decorator.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 响应装饰器
|
||||
* 基于 NestJS 装饰器实现
|
||||
* 对应 Java: @ResponseBody
|
||||
*/
|
||||
|
||||
export const RESPONSE_MESSAGE_KEY = 'response_message';
|
||||
export const RESPONSE_CODE_KEY = 'response_code';
|
||||
|
||||
/**
|
||||
* 设置响应消息
|
||||
*/
|
||||
export const ResponseMessage = (message: string) =>
|
||||
SetMetadata(RESPONSE_MESSAGE_KEY, message);
|
||||
|
||||
/**
|
||||
* 设置响应代码
|
||||
*/
|
||||
export const ResponseCode = (code: string) =>
|
||||
SetMetadata(RESPONSE_CODE_KEY, code);
|
||||
|
||||
/**
|
||||
* 成功响应装饰器
|
||||
*/
|
||||
export const SuccessResponse = (
|
||||
message: string = '操作成功',
|
||||
code: string = 'SUCCESS',
|
||||
) => {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
SetMetadata(RESPONSE_MESSAGE_KEY, message)(target, propertyKey, descriptor);
|
||||
SetMetadata(RESPONSE_CODE_KEY, code)(target, propertyKey, descriptor);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 分页响应装饰器
|
||||
*/
|
||||
export const PaginatedResponse = (message: string = '查询成功') => {
|
||||
return SetMetadata(RESPONSE_MESSAGE_KEY, message);
|
||||
};
|
||||
59
src/common/response/response.interceptor.ts
Normal file
59
src/common/response/response.interceptor.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Result } from './result.class';
|
||||
import { RESPONSE_MESSAGE_KEY, RESPONSE_CODE_KEY } from './response.decorator';
|
||||
|
||||
/**
|
||||
* 响应拦截器
|
||||
* 基于 NestJS 拦截器实现
|
||||
* 与PHP/Java框架保持基本一致的响应格式,并添加timestamp字段
|
||||
*
|
||||
* PHP格式: {data, msg, code}
|
||||
* Java格式: {code, msg, data}
|
||||
* NestJS格式: {code, msg, data, timestamp} (与Java基本一致,添加timestamp)
|
||||
*/
|
||||
@Injectable()
|
||||
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const responseMessage = this.reflector.getAllAndOverride<string>(
|
||||
RESPONSE_MESSAGE_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
const responseCode = this.reflector.getAllAndOverride<string>(
|
||||
RESPONSE_CODE_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
// 如果数据已经是 Result 格式,直接返回
|
||||
if (data && typeof data === 'object' && 'code' in data && 'msg' in data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 如果是分页数据
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'data' in data &&
|
||||
'meta' in data
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 普通数据包装 - 使用与PHP/Java一致的格式
|
||||
return Result.success(data, responseMessage || '操作成功');
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
19
src/common/response/response.module.ts
Normal file
19
src/common/response/response.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ResponseInterceptor } from './response.interceptor';
|
||||
|
||||
/**
|
||||
* 响应处理模块 - 基础设施层
|
||||
* 基于 NestJS 拦截器实现
|
||||
* 对应 Java: ResponseHandler
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ResponseInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class ResponseModule {}
|
||||
102
src/common/response/result.class.ts
Normal file
102
src/common/response/result.class.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
PaginationMeta,
|
||||
} from './result.interface';
|
||||
|
||||
/**
|
||||
* 统一响应结果类
|
||||
* 基于 NestJS 响应处理实现
|
||||
* 与PHP/Java框架保持基本一致的格式,并添加timestamp字段
|
||||
*
|
||||
* PHP格式: {data, msg, code}
|
||||
* Java格式: {code, msg, data}
|
||||
* NestJS格式: {code, msg, data, timestamp} (与Java基本一致,添加timestamp)
|
||||
*/
|
||||
export class Result<T = any> {
|
||||
public readonly code: number;
|
||||
public readonly msg: string;
|
||||
public readonly data?: T;
|
||||
public readonly timestamp: string;
|
||||
|
||||
constructor(code: number, msg: string, data?: T) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.timestamp = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
* 对应PHP: success($msg, $data, $code)
|
||||
* 对应Java: Result.success($msg, $data)
|
||||
*/
|
||||
static success<T>(data?: T, msg: string = '操作成功'): ApiResponse<T> {
|
||||
return new Result(1, msg, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
* 对应PHP: fail($msg, $data, $code)
|
||||
* 对应Java: Result.fail($msg, $data)
|
||||
*/
|
||||
static fail(msg: string = '操作失败', data?: any): ApiResponse {
|
||||
return new Result(0, msg, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
* 对应PHP/Java的分页数据格式
|
||||
*/
|
||||
static paginated<T>(
|
||||
data: T[],
|
||||
page: number,
|
||||
limit: number,
|
||||
total: number,
|
||||
msg: string = '查询成功',
|
||||
): PaginatedResponse<T> {
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
const meta: PaginationMeta = {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
};
|
||||
|
||||
return {
|
||||
code: 1,
|
||||
msg,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建响应
|
||||
* 对应PHP: Response::create(['data' => $data, 'msg' => $msg, 'code' => $code])
|
||||
* 对应Java: Result.instance($code, $msg, $data)
|
||||
*/
|
||||
static create<T>(
|
||||
code: number,
|
||||
msg: string,
|
||||
data?: T,
|
||||
): ApiResponse<T> {
|
||||
return new Result(code, msg, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 JSON
|
||||
* 与PHP/Java基本一致,添加timestamp字段
|
||||
*/
|
||||
toJSON(): ApiResponse<T> {
|
||||
return {
|
||||
code: this.code,
|
||||
msg: this.msg,
|
||||
data: this.data,
|
||||
timestamp: this.timestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
43
src/common/response/result.interface.ts
Normal file
43
src/common/response/result.interface.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 统一响应接口
|
||||
* 与PHP/Java框架保持基本一致的格式,并添加timestamp字段
|
||||
*
|
||||
* PHP格式: {data, msg, code}
|
||||
* Java格式: {code, msg, data}
|
||||
* NestJS格式: {code, msg, data, timestamp} (与Java基本一致,添加timestamp)
|
||||
*/
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: T;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface PaginationMeta {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T = any> extends ApiResponse<T[]> {
|
||||
meta: PaginationMeta;
|
||||
}
|
||||
|
||||
export interface ErrorResponse extends ApiResponse {
|
||||
code: 0;
|
||||
msg: string;
|
||||
data?: any;
|
||||
timestamp: string;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
statusCode: number;
|
||||
timestamp: string;
|
||||
path?: string;
|
||||
details?: any;
|
||||
};
|
||||
}
|
||||
14
src/common/scheduler/scheduler.module.ts
Normal file
14
src/common/scheduler/scheduler.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
/**
|
||||
* 调度模块 - 基础设施层
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/techniques/task-scheduling
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ScheduleModule.forRoot()],
|
||||
exports: [ScheduleModule],
|
||||
})
|
||||
export class SchedulerModule {}
|
||||
8
src/common/security/decorators/public.decorator.ts
Normal file
8
src/common/security/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 公开路由装饰器
|
||||
* 标记路由为公开访问,跳过认证
|
||||
*/
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
8
src/common/security/decorators/roles.decorator.ts
Normal file
8
src/common/security/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 角色装饰器
|
||||
* 标记路由需要的角色权限
|
||||
*/
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
31
src/common/security/guards/jwt-auth.guard.ts
Normal file
31
src/common/security/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
/**
|
||||
* JWT 认证守卫
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/security/authentication
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路由是否标记为公开
|
||||
*/
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
34
src/common/security/guards/roles.guard.ts
Normal file
34
src/common/security/guards/roles.guard.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
/**
|
||||
* 角色守卫
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/security/authorization
|
||||
*/
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
/**
|
||||
* 检查用户角色权限
|
||||
*/
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return requiredRoles.some((role) => user.roles?.includes(role));
|
||||
}
|
||||
}
|
||||
37
src/common/security/security.module.ts
Normal file
37
src/common/security/security.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { LocalStrategy } from './strategies/local.strategy';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { RolesGuard } from './guards/roles.guard';
|
||||
|
||||
/**
|
||||
* 安全模块 - 基础设施层
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/security/authentication
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const jwtConfig = configService.get('jwt');
|
||||
|
||||
return {
|
||||
secret: jwtConfig.secret,
|
||||
signOptions: {
|
||||
expiresIn: jwtConfig.expiresIn,
|
||||
algorithm: jwtConfig.algorithm,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [JwtStrategy, LocalStrategy, JwtAuthGuard, RolesGuard],
|
||||
exports: [JwtModule, PassportModule, JwtAuthGuard, RolesGuard],
|
||||
})
|
||||
export class SecurityModule {}
|
||||
38
src/common/security/strategies/jwt.strategy.ts
Normal file
38
src/common/security/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
|
||||
/**
|
||||
* JWT 策略
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/security/authentication
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get<string>('jwt.secret'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 JWT 载荷
|
||||
*/
|
||||
async validate(payload: any) {
|
||||
// 这里可以添加额外的验证逻辑
|
||||
// 例如:检查用户是否仍然存在、是否被禁用等
|
||||
if (!payload.sub) {
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
|
||||
return {
|
||||
userId: payload.sub,
|
||||
username: payload.username,
|
||||
roles: payload.roles || [],
|
||||
siteId: payload.siteId,
|
||||
};
|
||||
}
|
||||
}
|
||||
36
src/common/security/strategies/local.strategy.ts
Normal file
36
src/common/security/strategies/local.strategy.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
|
||||
/**
|
||||
* 本地认证策略
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/security/authentication
|
||||
*/
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
super({
|
||||
usernameField: 'username',
|
||||
passwordField: 'password',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户凭据
|
||||
*/
|
||||
async validate(username: string, password: string): Promise<any> {
|
||||
// 这里应该调用用户服务来验证凭据
|
||||
// 暂时返回模拟数据,实际实现时需要注入用户服务
|
||||
if (username === 'admin' && password === 'admin') {
|
||||
return {
|
||||
userId: 1,
|
||||
username: 'admin',
|
||||
roles: ['admin'],
|
||||
siteId: 1,
|
||||
};
|
||||
}
|
||||
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
}
|
||||
15
src/common/swagger/swagger.module.ts
Normal file
15
src/common/swagger/swagger.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { SwaggerService } from './swagger.service';
|
||||
|
||||
/**
|
||||
* Swagger 文档模块 - 基础设施层
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/recipes/swagger
|
||||
*/
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [SwaggerService],
|
||||
exports: [SwaggerService],
|
||||
})
|
||||
export class SwaggerModule {}
|
||||
78
src/common/swagger/swagger.service.ts
Normal file
78
src/common/swagger/swagger.service.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Swagger 文档服务
|
||||
* 基于 NestJS 官方文档实现
|
||||
* 参考: https://docs.nestjs.cn/recipes/swagger
|
||||
*/
|
||||
@Injectable()
|
||||
export class SwaggerService {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 初始化 Swagger 文档
|
||||
*/
|
||||
setupSwagger(app: INestApplication) {
|
||||
const swaggerConfig = this.configService.get('swagger');
|
||||
|
||||
if (!swaggerConfig.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle(swaggerConfig.title)
|
||||
.setDescription(swaggerConfig.description)
|
||||
.setVersion(swaggerConfig.version)
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
name: 'JWT',
|
||||
description: 'Enter JWT token',
|
||||
in: 'header',
|
||||
},
|
||||
'JWT-auth',
|
||||
)
|
||||
.addTag('认证管理', '用户认证相关接口')
|
||||
.addTag('系统管理', '系统配置相关接口')
|
||||
.addTag('用户管理', '用户管理相关接口')
|
||||
.addTag('文档', 'API 文档相关接口')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
|
||||
// 设置 Swagger UI 选项
|
||||
const options = {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
displayRequestDuration: true,
|
||||
filter: true,
|
||||
showExtensions: true,
|
||||
showCommonExtensions: true,
|
||||
},
|
||||
};
|
||||
|
||||
SwaggerModule.setup('api-docs', app, document, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Swagger 文档 JSON
|
||||
*/
|
||||
getDocument(app: INestApplication) {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle(this.configService.get('swagger.title') || 'WWJCloud API')
|
||||
.setDescription(
|
||||
this.configService.get('swagger.description') ||
|
||||
'WWJCloud API Documentation',
|
||||
)
|
||||
.setVersion(this.configService.get('swagger.version') || '1.0.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
return SwaggerModule.createDocument(app, config);
|
||||
}
|
||||
}
|
||||
14
src/common/system/system.module.ts
Normal file
14
src/common/system/system.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { SystemUtils } from './system.utils';
|
||||
|
||||
/**
|
||||
* 系统工具模块 - 基础设施层
|
||||
* 基于 NestJS 实现 Java 风格的 SystemUtils
|
||||
* 对应 Java: SystemUtils
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [SystemUtils],
|
||||
exports: [SystemUtils],
|
||||
})
|
||||
export class SystemModule {}
|
||||
170
src/common/system/system.utils.ts
Normal file
170
src/common/system/system.utils.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* 系统工具类
|
||||
* 基于 NestJS 实现 Java 风格的 SystemUtils
|
||||
* 对应 Java: SystemUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class SystemUtils {
|
||||
private readonly logger = new Logger(SystemUtils.name);
|
||||
private readonly moduleMap = new Map<string, any>();
|
||||
|
||||
// 模块名称常量
|
||||
static readonly I18N = 'i18n';
|
||||
static readonly LOADER = 'loader';
|
||||
static readonly CACHE = 'cache';
|
||||
static readonly QUEUE = 'queue';
|
||||
static readonly EVENT = 'event';
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.initializeModules();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模块
|
||||
* 对应 Java: SystemUtils 构造函数
|
||||
*/
|
||||
private initializeModules(): void {
|
||||
// 这里会在模块加载时注入相应的服务
|
||||
this.logger.log('SystemUtils 模块初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册模块
|
||||
* @param name 模块名称
|
||||
* @param module 模块实例
|
||||
*/
|
||||
registerModule(name: string, module: any): void {
|
||||
this.moduleMap.set(name, module);
|
||||
this.logger.log(`注册模块: ${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块
|
||||
* @param name 模块名称
|
||||
* @returns 模块实例
|
||||
*/
|
||||
getModule<T = any>(name: string): T | null {
|
||||
const module = this.moduleMap.get(name);
|
||||
if (!module) {
|
||||
this.logger.warn(`未找到模块: ${name}`);
|
||||
return null;
|
||||
}
|
||||
return module as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有模块名称
|
||||
* @returns 模块名称集合
|
||||
*/
|
||||
getModuleNames(): Set<string> {
|
||||
return new Set(this.moduleMap.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过类名创建实例
|
||||
* @param className 类名
|
||||
* @returns 实例
|
||||
*/
|
||||
static forName<T = any>(className: string): T | null {
|
||||
try {
|
||||
// 在 Node.js 环境中,这需要动态导入
|
||||
// 实际使用时需要根据具体需求实现
|
||||
console.warn('forName 方法需要根据具体需求实现动态类加载');
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`创建实例失败: ${className}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Windows 系统
|
||||
* @returns 是否为 Windows
|
||||
*/
|
||||
static isWindowsOS(): boolean {
|
||||
return process.platform === 'win32';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Linux 系统
|
||||
* @returns 是否为 Linux
|
||||
*/
|
||||
static isLinuxOS(): boolean {
|
||||
return process.platform === 'linux';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作系统名称
|
||||
* @returns 操作系统名称
|
||||
*/
|
||||
static getOSName(): string {
|
||||
return process.platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启应用
|
||||
* 对应 Java: SystemUtils.restart()
|
||||
*/
|
||||
static restart(): void {
|
||||
try {
|
||||
if (SystemUtils.isWindowsOS()) {
|
||||
// Windows 重启脚本
|
||||
const { exec } = require('child_process');
|
||||
exec('restart.bat', (error: any) => {
|
||||
if (error) {
|
||||
console.error('重启失败:', error);
|
||||
}
|
||||
});
|
||||
} else if (SystemUtils.isLinuxOS()) {
|
||||
// Linux 重启脚本
|
||||
const { exec } = require('child_process');
|
||||
exec('restart.sh', (error: any) => {
|
||||
if (error) {
|
||||
console.error('重启失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重启应用失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
* @returns 系统信息
|
||||
*/
|
||||
static getSystemInfo(): {
|
||||
platform: string;
|
||||
arch: string;
|
||||
nodeVersion: string;
|
||||
pid: number;
|
||||
uptime: number;
|
||||
} {
|
||||
return {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
nodeVersion: process.version,
|
||||
pid: process.pid,
|
||||
uptime: process.uptime(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用情况
|
||||
* @returns 内存使用情况
|
||||
*/
|
||||
static getMemoryUsage(): NodeJS.MemoryUsage {
|
||||
return process.memoryUsage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 CPU 使用情况
|
||||
* @returns CPU 使用情况
|
||||
*/
|
||||
static getCPUUsage(): NodeJS.CpuUsage {
|
||||
return process.cpuUsage();
|
||||
}
|
||||
}
|
||||
15
src/common/tracing/tracing.module.ts
Normal file
15
src/common/tracing/tracing.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TracingService } from './tracing.service';
|
||||
|
||||
/**
|
||||
* 链路追踪模块 - 基础设施层
|
||||
* 提供请求追踪和性能监控功能
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [TracingService],
|
||||
exports: [TracingService],
|
||||
})
|
||||
export class TracingModule {}
|
||||
76
src/common/tracing/tracing.service.ts
Normal file
76
src/common/tracing/tracing.service.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* 链路追踪服务
|
||||
* 提供请求追踪和性能监控功能
|
||||
*/
|
||||
@Injectable()
|
||||
export class TracingService {
|
||||
private readonly logger = new Logger(TracingService.name);
|
||||
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 开始追踪
|
||||
*/
|
||||
startTrace(traceId: string, operation: string) {
|
||||
const startTime = Date.now();
|
||||
this.logger.log(`[${traceId}] Starting ${operation} at ${startTime}`);
|
||||
|
||||
return {
|
||||
traceId,
|
||||
operation,
|
||||
startTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束追踪
|
||||
*/
|
||||
endTrace(trace: any, result?: any) {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - trace.startTime;
|
||||
|
||||
this.logger.log(
|
||||
`[${trace.traceId}] Completed ${trace.operation} in ${duration}ms`,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
this.logger.debug(`[${trace.traceId}] Result: ${JSON.stringify(result)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
...trace,
|
||||
endTime,
|
||||
duration,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误
|
||||
*/
|
||||
recordError(trace: any, error: Error) {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - trace.startTime;
|
||||
|
||||
this.logger.error(
|
||||
`[${trace.traceId}] Error in ${trace.operation} after ${duration}ms: ${error.message}`,
|
||||
);
|
||||
|
||||
return {
|
||||
...trace,
|
||||
endTime,
|
||||
duration,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成追踪 ID
|
||||
*/
|
||||
generateTraceId(): string {
|
||||
return `trace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
157
src/common/utils/clone.util.ts
Normal file
157
src/common/utils/clone.util.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 深度克隆工具类
|
||||
* 统一管理深度克隆功能,避免重复实现
|
||||
* 对应 Java: ObjectUtils.clone()
|
||||
*/
|
||||
@Injectable()
|
||||
export class CloneUtil {
|
||||
/**
|
||||
* 深度克隆对象
|
||||
* @param obj 要克隆的对象
|
||||
* @returns 克隆后的对象
|
||||
*/
|
||||
deepClone<T = any>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as any;
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map((item) => this.deepClone(item)) as any;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {} as any;
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = this.deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return clonedObj;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 JSON 序列化进行深度克隆
|
||||
* 性能更好,但有限制(不能克隆函数、undefined、Symbol等)
|
||||
* @param obj 要克隆的对象
|
||||
* @returns 克隆后的对象
|
||||
*/
|
||||
jsonClone<T = any>(obj: T): T {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to clone object using JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浅克隆对象
|
||||
* @param obj 要克隆的对象
|
||||
* @returns 克隆后的对象
|
||||
*/
|
||||
shallowClone<T = any>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return [...obj] as any;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
return { ...obj };
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆数组
|
||||
* @param arr 要克隆的数组
|
||||
* @returns 克隆后的数组
|
||||
*/
|
||||
cloneArray<T>(arr: T[]): T[] {
|
||||
if (!Array.isArray(arr)) {
|
||||
throw new Error('Input is not an array');
|
||||
}
|
||||
return arr.map((item) => this.deepClone(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆对象并过滤指定属性
|
||||
* @param obj 要克隆的对象
|
||||
* @param excludeKeys 要排除的属性名数组
|
||||
* @returns 克隆后的对象
|
||||
*/
|
||||
cloneExclude<T = any>(obj: T, excludeKeys: string[]): T {
|
||||
const cloned = this.deepClone(obj);
|
||||
|
||||
if (typeof cloned === 'object' && cloned !== null) {
|
||||
for (const key of excludeKeys) {
|
||||
delete (cloned as any)[key];
|
||||
}
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆对象并只保留指定属性
|
||||
* @param obj 要克隆的对象
|
||||
* @param includeKeys 要保留的属性名数组
|
||||
* @returns 克隆后的对象
|
||||
*/
|
||||
cloneInclude<T = any>(obj: T, includeKeys: string[]): Partial<T> {
|
||||
const cloned = this.deepClone(obj);
|
||||
const result: any = {};
|
||||
|
||||
if (typeof cloned === 'object' && cloned !== null) {
|
||||
for (const key of includeKeys) {
|
||||
if (key in cloned) {
|
||||
result[key] = (cloned as any)[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否可克隆
|
||||
* @param obj 要检查的对象
|
||||
* @returns 是否可克隆
|
||||
*/
|
||||
isCloneable(obj: any): boolean {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否包含不可序列化的内容
|
||||
try {
|
||||
JSON.stringify(obj);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全克隆(如果不可克隆则返回原对象)
|
||||
* @param obj 要克隆的对象
|
||||
* @returns 克隆后的对象或原对象
|
||||
*/
|
||||
safeClone<T = any>(obj: T): T {
|
||||
if (!this.isCloneable(obj)) {
|
||||
return obj;
|
||||
}
|
||||
return this.deepClone(obj);
|
||||
}
|
||||
}
|
||||
317
src/common/utils/crypto.util.ts
Normal file
317
src/common/utils/crypto.util.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import * as crypto from 'crypto';
|
||||
import * as CryptoJS from 'crypto-js';
|
||||
|
||||
/**
|
||||
* 加密工具类
|
||||
* 对应 Java: PasswordEncipher
|
||||
*/
|
||||
@Injectable()
|
||||
export class CryptoUtil {
|
||||
/**
|
||||
* 生成随机盐值
|
||||
*/
|
||||
static generateSalt(rounds: number = 10): string {
|
||||
return bcrypt.genSaltSync(rounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码加密
|
||||
*/
|
||||
static hashPassword(password: string, salt?: string): string {
|
||||
if (salt) {
|
||||
return bcrypt.hashSync(password, salt);
|
||||
}
|
||||
return bcrypt.hashSync(password, this.generateSalt());
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码验证
|
||||
*/
|
||||
static verifyPassword(password: string, hashedPassword: string): boolean {
|
||||
return bcrypt.compareSync(password, hashedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 加密
|
||||
*/
|
||||
static md5(text: string): string {
|
||||
return crypto.createHash('md5').update(text).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA1 加密
|
||||
*/
|
||||
static sha1(text: string): string {
|
||||
return crypto.createHash('sha1').update(text).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA256 加密
|
||||
*/
|
||||
static sha256(text: string): string {
|
||||
return crypto.createHash('sha256').update(text).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA512 加密
|
||||
*/
|
||||
static sha512(text: string): string {
|
||||
return crypto.createHash('sha512').update(text).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
static randomString(length: number = 32): string {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 加密
|
||||
*/
|
||||
static aesEncrypt(text: string, key: string): string {
|
||||
return CryptoJS.AES.encrypt(text, key).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 解密
|
||||
*/
|
||||
static aesDecrypt(encryptedText: string, key: string): string {
|
||||
const bytes = CryptoJS.AES.decrypt(encryptedText, key);
|
||||
return bytes.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
/**
|
||||
* DES 加密
|
||||
*/
|
||||
static desEncrypt(text: string, key: string): string {
|
||||
return CryptoJS.DES.encrypt(text, key).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* DES 解密
|
||||
*/
|
||||
static desDecrypt(encryptedText: string, key: string): string {
|
||||
const bytes = CryptoJS.DES.decrypt(encryptedText, key);
|
||||
return bytes.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 编码
|
||||
*/
|
||||
static base64Encode(text: string): string {
|
||||
return Buffer.from(text, 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 解码
|
||||
*/
|
||||
static base64Decode(encodedText: string): string {
|
||||
return Buffer.from(encodedText, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 安全的 Base64 编码
|
||||
*/
|
||||
static base64UrlEncode(text: string): string {
|
||||
return Buffer.from(text, 'utf8')
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 安全的 Base64 解码
|
||||
*/
|
||||
static base64UrlDecode(encodedText: string): string {
|
||||
// 补齐填充字符
|
||||
const padded = encodedText + '='.repeat((4 - (encodedText.length % 4)) % 4);
|
||||
const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
|
||||
return Buffer.from(base64, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 HMAC
|
||||
*/
|
||||
static hmac(text: string, key: string, algorithm: string = 'sha256'): string {
|
||||
return crypto.createHmac(algorithm, key).update(text).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JWT Token
|
||||
*/
|
||||
static generateJwtToken(
|
||||
payload: any,
|
||||
secret: string,
|
||||
expiresIn: string = '1h',
|
||||
): string {
|
||||
const header = {
|
||||
alg: 'HS256',
|
||||
typ: 'JWT',
|
||||
};
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const exp = now + this.parseExpiresIn(expiresIn);
|
||||
|
||||
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
||||
const encodedPayload = this.base64UrlEncode(
|
||||
JSON.stringify({ ...payload, exp }),
|
||||
);
|
||||
const signature = this.hmac(`${encodedHeader}.${encodedPayload}`, secret);
|
||||
|
||||
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 JWT Token
|
||||
*/
|
||||
static verifyJwtToken(token: string, secret: string): any {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid token format');
|
||||
}
|
||||
|
||||
const [header, payload, signature] = parts;
|
||||
const expectedSignature = this.hmac(`${header}.${payload}`, secret);
|
||||
|
||||
if (signature !== expectedSignature) {
|
||||
throw new Error('Invalid token signature');
|
||||
}
|
||||
|
||||
const decodedPayload = JSON.parse(this.base64UrlDecode(payload));
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (decodedPayload.exp && decodedPayload.exp < now) {
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
|
||||
return decodedPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析过期时间
|
||||
*/
|
||||
private static parseExpiresIn(expiresIn: string): number {
|
||||
const unit = expiresIn.slice(-1);
|
||||
const value = parseInt(expiresIn.slice(0, -1));
|
||||
|
||||
switch (unit) {
|
||||
case 's':
|
||||
return value;
|
||||
case 'm':
|
||||
return value * 60;
|
||||
case 'h':
|
||||
return value * 60 * 60;
|
||||
case 'd':
|
||||
return value * 60 * 60 * 24;
|
||||
default:
|
||||
return 3600; // 默认1小时
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 API 签名
|
||||
*/
|
||||
static generateApiSignature(
|
||||
params: Record<string, any>,
|
||||
secret: string,
|
||||
): string {
|
||||
// 按参数名排序
|
||||
const sortedParams = Object.keys(params)
|
||||
.sort()
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
return this.hmac(sortedParams, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 API 签名
|
||||
*/
|
||||
static verifyApiSignature(
|
||||
params: Record<string, any>,
|
||||
signature: string,
|
||||
secret: string,
|
||||
): boolean {
|
||||
const expectedSignature = this.generateApiSignature(params, secret);
|
||||
return signature === expectedSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件哈希
|
||||
*/
|
||||
static generateFileHash(
|
||||
buffer: Buffer,
|
||||
algorithm: string = 'sha256',
|
||||
): string {
|
||||
return crypto.createHash(algorithm).update(buffer).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密码强度
|
||||
*/
|
||||
static getPasswordStrength(password: string): {
|
||||
score: number;
|
||||
level: 'weak' | 'medium' | 'strong' | 'very-strong';
|
||||
suggestions: string[];
|
||||
} {
|
||||
let score = 0;
|
||||
const suggestions: string[] = [];
|
||||
|
||||
// 长度检查
|
||||
if (password.length >= 8) {
|
||||
score += 1;
|
||||
} else {
|
||||
suggestions.push('密码长度至少8位');
|
||||
}
|
||||
|
||||
// 包含小写字母
|
||||
if (/[a-z]/.test(password)) {
|
||||
score += 1;
|
||||
} else {
|
||||
suggestions.push('包含小写字母');
|
||||
}
|
||||
|
||||
// 包含大写字母
|
||||
if (/[A-Z]/.test(password)) {
|
||||
score += 1;
|
||||
} else {
|
||||
suggestions.push('包含大写字母');
|
||||
}
|
||||
|
||||
// 包含数字
|
||||
if (/\d/.test(password)) {
|
||||
score += 1;
|
||||
} else {
|
||||
suggestions.push('包含数字');
|
||||
}
|
||||
|
||||
// 包含特殊字符
|
||||
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
score += 1;
|
||||
} else {
|
||||
suggestions.push('包含特殊字符');
|
||||
}
|
||||
|
||||
// 长度超过12位
|
||||
if (password.length >= 12) {
|
||||
score += 1;
|
||||
}
|
||||
|
||||
let level: 'weak' | 'medium' | 'strong' | 'very-strong';
|
||||
if (score <= 2) {
|
||||
level = 'weak';
|
||||
} else if (score <= 3) {
|
||||
level = 'medium';
|
||||
} else if (score <= 4) {
|
||||
level = 'strong';
|
||||
} else {
|
||||
level = 'very-strong';
|
||||
}
|
||||
|
||||
return { score, level, suggestions };
|
||||
}
|
||||
}
|
||||
8
src/common/utils/index.ts
Normal file
8
src/common/utils/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './crypto.util';
|
||||
export * from './system.util';
|
||||
export * from './reflect.util';
|
||||
export * from './object.util';
|
||||
export * from './json.util';
|
||||
export * from './request.util';
|
||||
export * from './clone.util';
|
||||
export * from './utils.module';
|
||||
456
src/common/utils/json.util.ts
Normal file
456
src/common/utils/json.util.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* JSON工具类
|
||||
* 基于 NestJS 实现 Java 风格的 JacksonUtils
|
||||
* 对应 Java: JacksonUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class JsonUtil {
|
||||
/**
|
||||
* 将对象转换为JSON字符串
|
||||
* @param obj 要转换的对象
|
||||
* @param pretty 是否格式化输出
|
||||
* @returns JSON字符串
|
||||
*/
|
||||
toJsonString(obj: any, pretty: boolean = false): string {
|
||||
try {
|
||||
if (pretty) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
return JSON.stringify(obj);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to convert object to JSON string: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将JSON字符串转换为对象
|
||||
* @param jsonString JSON字符串
|
||||
* @param targetClass 目标类(可选)
|
||||
* @returns 转换后的对象
|
||||
*/
|
||||
fromJsonString<T = any>(
|
||||
jsonString: string,
|
||||
targetClass?: new (...args: any[]) => T,
|
||||
): T | null {
|
||||
try {
|
||||
const obj = JSON.parse(jsonString);
|
||||
if (targetClass) {
|
||||
return this.toObject(obj, targetClass);
|
||||
}
|
||||
return obj;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to convert JSON string to object: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为指定类型的对象
|
||||
* @param obj 源对象
|
||||
* @param targetClass 目标类
|
||||
* @returns 转换后的对象
|
||||
*/
|
||||
toObject<T = any>(
|
||||
obj: any,
|
||||
targetClass: new (...args: any[]) => T,
|
||||
): T | null {
|
||||
if (!obj || !targetClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const target = new targetClass();
|
||||
this.copyProperties(obj, target);
|
||||
return target;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to convert object to target class: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象列表转换为指定类型的对象列表
|
||||
* @param objList 源对象列表
|
||||
* @param targetClass 目标类
|
||||
* @returns 转换后的对象列表
|
||||
*/
|
||||
toObjectList<T = any>(
|
||||
objList: any[],
|
||||
targetClass: new (...args: any[]) => T,
|
||||
): T[] {
|
||||
if (!objList || !Array.isArray(objList)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: T[] = [];
|
||||
for (const obj of objList) {
|
||||
try {
|
||||
const target = this.toObject(obj, targetClass);
|
||||
if (target) {
|
||||
result.push(target);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to convert object to target class: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制对象属性
|
||||
* @param source 源对象
|
||||
* @param target 目标对象
|
||||
*/
|
||||
private copyProperties(source: any, target: any): void {
|
||||
if (
|
||||
!source ||
|
||||
!target ||
|
||||
typeof source !== 'object' ||
|
||||
typeof target !== 'object'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key) && target.hasOwnProperty(key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为下划线命名的JSON字符串
|
||||
* @param obj 要转换的对象
|
||||
* @param pretty 是否格式化输出
|
||||
* @returns 下划线命名的JSON字符串
|
||||
*/
|
||||
toSnakeCaseJsonString(obj: any, pretty: boolean = false): string {
|
||||
try {
|
||||
const snakeCaseObj = this.toSnakeCase(obj);
|
||||
return this.toJsonString(snakeCaseObj, pretty);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to convert object to snake case JSON string: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将下划线命名的JSON字符串转换为对象
|
||||
* @param jsonString 下划线命名的JSON字符串
|
||||
* @param targetClass 目标类(可选)
|
||||
* @returns 转换后的对象
|
||||
*/
|
||||
fromSnakeCaseJsonString<T = any>(
|
||||
jsonString: string,
|
||||
targetClass?: new (...args: any[]) => T,
|
||||
): T | null {
|
||||
try {
|
||||
const obj = JSON.parse(jsonString);
|
||||
const camelCaseObj = this.toCamelCase(obj);
|
||||
if (targetClass) {
|
||||
return this.toObject(camelCaseObj, targetClass);
|
||||
}
|
||||
return camelCaseObj;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to convert snake case JSON string to object: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为下划线命名格式
|
||||
* @param obj 要转换的对象
|
||||
* @returns 下划线命名格式的对象
|
||||
*/
|
||||
toSnakeCase(obj: any): any {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => this.toSnakeCase(item));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const result: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const snakeKey = this.camelToSnake(key);
|
||||
result[snakeKey] = this.toSnakeCase(obj[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为驼峰命名格式
|
||||
* @param obj 要转换的对象
|
||||
* @returns 驼峰命名格式的对象
|
||||
*/
|
||||
toCamelCase(obj: any): any {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => this.toCamelCase(item));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const result: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const camelKey = this.snakeToCamel(key);
|
||||
result[camelKey] = this.toCamelCase(obj[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转下划线
|
||||
* @param str 驼峰字符串
|
||||
* @returns 下划线字符串
|
||||
*/
|
||||
private camelToSnake(str: string): string {
|
||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
* @param str 下划线字符串
|
||||
* @returns 驼峰字符串
|
||||
*/
|
||||
private snakeToCamel(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字符串是否为有效的JSON
|
||||
* @param str 要检查的字符串
|
||||
* @returns 是否为有效的JSON
|
||||
*/
|
||||
isValidJson(str: string): boolean {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地将JSON字符串转换为对象
|
||||
* @param jsonString JSON字符串
|
||||
* @param defaultValue 默认值
|
||||
* @returns 转换后的对象或默认值
|
||||
*/
|
||||
safeFromJsonString<T = any>(jsonString: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并JSON对象
|
||||
* @param target 目标对象
|
||||
* @param sources 源对象数组
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
mergeJson<T = any>(target: T, ...sources: any[]): T {
|
||||
if (!target) {
|
||||
target = {} as T;
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
if (source && typeof source === 'object') {
|
||||
this.deepMerge(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并对象
|
||||
* @param target 目标对象
|
||||
* @param source 源对象
|
||||
*/
|
||||
private deepMerge(target: any, source: any): void {
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = target[key];
|
||||
|
||||
if (this.isObject(sourceValue) && this.isObject(targetValue)) {
|
||||
this.deepMerge(targetValue, sourceValue);
|
||||
} else {
|
||||
target[key] = JSON.parse(JSON.stringify(sourceValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为对象
|
||||
* @param obj 要检查的值
|
||||
* @returns 是否为对象
|
||||
*/
|
||||
private isObject(obj: any): boolean {
|
||||
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取JSON对象的路径值
|
||||
* @param obj JSON对象
|
||||
* @param path 路径,支持点号分隔
|
||||
* @param defaultValue 默认值
|
||||
* @returns 路径值
|
||||
*/
|
||||
getPathValue<T = any>(
|
||||
obj: any,
|
||||
path: string,
|
||||
defaultValue?: T,
|
||||
): T | undefined {
|
||||
if (!obj || !path) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return current !== undefined ? current : defaultValue;
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置JSON对象的路径值
|
||||
* @param obj JSON对象
|
||||
* @param path 路径,支持点号分隔
|
||||
* @param value 值
|
||||
*/
|
||||
setPathValue(obj: any, path: string, value: any): void {
|
||||
if (!obj || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = path.split('.');
|
||||
const lastKey = keys.pop();
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current[key] === null || current[key] === undefined) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
if (lastKey) {
|
||||
current[lastKey] = value;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set path value ${path}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除JSON对象的路径值
|
||||
* @param obj JSON对象
|
||||
* @param path 路径,支持点号分隔
|
||||
*/
|
||||
deletePathValue(obj: any, path: string): void {
|
||||
if (!obj || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = path.split('.');
|
||||
const lastKey = keys.pop();
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current[key] === null || current[key] === undefined) {
|
||||
return;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
if (lastKey && current.hasOwnProperty(lastKey)) {
|
||||
delete current[lastKey];
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete path value ${path}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤JSON对象
|
||||
* @param obj JSON对象
|
||||
* @param predicate 过滤条件函数
|
||||
* @returns 过滤后的对象
|
||||
*/
|
||||
filterJson(obj: any, predicate: (key: string, value: any) => boolean): any {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const filtered: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key) && predicate(key, obj[key])) {
|
||||
filtered[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射JSON对象
|
||||
* @param obj JSON对象
|
||||
* @param mapper 映射函数
|
||||
* @returns 映射后的对象
|
||||
*/
|
||||
mapJson(obj: any, mapper: (key: string, value: any) => any): any {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapped: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
mapped[key] = mapper(key, obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
474
src/common/utils/object.util.ts
Normal file
474
src/common/utils/object.util.ts
Normal file
@@ -0,0 +1,474 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ReflectUtil } from './reflect.util';
|
||||
|
||||
/**
|
||||
* 对象处理工具类
|
||||
* 基于 NestJS 实现 Java 风格的 ObjectUtils
|
||||
* 对应 Java: ObjectUtils, CollectUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class ObjectUtil {
|
||||
constructor(private readonly reflectUtil: ReflectUtil) {}
|
||||
|
||||
/**
|
||||
* 创建对象实例
|
||||
* @param className 类名
|
||||
* @returns 对象实例
|
||||
*/
|
||||
create<T = any>(className: string): T {
|
||||
return this.reflectUtil.newInstance<T>(className);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数创建对象实例
|
||||
* @param className 类名
|
||||
* @param args 构造参数
|
||||
* @returns 对象实例
|
||||
*/
|
||||
createWithArgs<T = any>(className: string, ...args: any[]): T {
|
||||
return this.reflectUtil.newInstanceWithArgs<T>(className, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据源对象和目标类构建新对象
|
||||
* @param source 源对象
|
||||
* @param targetClass 目标类
|
||||
* @returns 新对象
|
||||
*/
|
||||
build<T = any>(
|
||||
source: any,
|
||||
targetClass: new (...args: any[]) => T,
|
||||
): T | null {
|
||||
if (!source || !targetClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const target = new targetClass();
|
||||
this.copyProperties(source, target);
|
||||
return target;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to build object: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制对象属性
|
||||
* @param source 源对象
|
||||
* @param target 目标对象
|
||||
* @returns 目标对象
|
||||
*/
|
||||
copyProperties<T = any>(source: any, target: T): T {
|
||||
if (!source || !target) {
|
||||
return target;
|
||||
}
|
||||
|
||||
try {
|
||||
this.reflectUtil.copyFields(source, target);
|
||||
return target;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy properties: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制指定属性
|
||||
* @param source 源对象
|
||||
* @param target 目标对象
|
||||
* @param fields 要复制的属性名数组
|
||||
* @returns 目标对象
|
||||
*/
|
||||
copyPropertiesWithFields<T = any>(
|
||||
source: any,
|
||||
target: T,
|
||||
fields: string[],
|
||||
): T {
|
||||
if (!source || !target || !fields) {
|
||||
return target;
|
||||
}
|
||||
|
||||
try {
|
||||
this.reflectUtil.copyFields(source, target, fields);
|
||||
return target;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to copy properties with fields: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为指定类型的对象列表
|
||||
* @param sourceList 源对象列表
|
||||
* @param targetClass 目标类
|
||||
* @returns 目标对象列表
|
||||
*/
|
||||
convertList<T = any>(
|
||||
sourceList: any[],
|
||||
targetClass: new (...args: any[]) => T,
|
||||
): T[] {
|
||||
if (!sourceList || !Array.isArray(sourceList)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetList: T[] = [];
|
||||
for (const source of sourceList) {
|
||||
try {
|
||||
const target = this.build(source, targetClass);
|
||||
if (target) {
|
||||
targetList.push(target);
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略转换失败的对象
|
||||
console.warn(`Failed to convert object: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return targetList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完全转换对象列表
|
||||
* @param sourceList 源对象列表
|
||||
* @param targetClass 目标类
|
||||
* @returns 目标对象列表
|
||||
*/
|
||||
convertListComplete<T = any>(
|
||||
sourceList: any[],
|
||||
targetClass: new (...args: any[]) => T,
|
||||
): T[] {
|
||||
if (!sourceList || !Array.isArray(sourceList)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetList: T[] = [];
|
||||
for (const source of sourceList) {
|
||||
try {
|
||||
const target = new targetClass();
|
||||
this.copyProperties(source, target);
|
||||
targetList.push(target);
|
||||
} catch (error) {
|
||||
// 忽略转换失败的对象
|
||||
console.warn(`Failed to convert object completely: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return targetList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否为空
|
||||
* @param obj 要检查的对象
|
||||
* @returns 是否为空
|
||||
*/
|
||||
isEmpty(obj: any): boolean {
|
||||
return this.reflectUtil.isEmpty(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否不为空
|
||||
* @param obj 要检查的对象
|
||||
* @returns 是否不为空
|
||||
*/
|
||||
isNotEmpty(obj: any): boolean {
|
||||
return this.reflectUtil.isNotEmpty(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象属性值
|
||||
* @param obj 对象
|
||||
* @param path 属性路径,支持点号分隔的嵌套属性
|
||||
* @param defaultValue 默认值
|
||||
* @returns 属性值
|
||||
*/
|
||||
getProperty<T = any>(
|
||||
obj: any,
|
||||
path: string,
|
||||
defaultValue?: T,
|
||||
): T | undefined {
|
||||
if (!obj || !path) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return current !== undefined ? current : defaultValue;
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置对象属性值
|
||||
* @param obj 对象
|
||||
* @param path 属性路径,支持点号分隔的嵌套属性
|
||||
* @param value 属性值
|
||||
*/
|
||||
setProperty(obj: any, path: string, value: any): void {
|
||||
if (!obj || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = path.split('.');
|
||||
const lastKey = keys.pop();
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current[key] === null || current[key] === undefined) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
if (lastKey) {
|
||||
current[lastKey] = value;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set property ${path}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除对象属性
|
||||
* @param obj 对象
|
||||
* @param path 属性路径,支持点号分隔的嵌套属性
|
||||
*/
|
||||
deleteProperty(obj: any, path: string): void {
|
||||
if (!obj || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = path.split('.');
|
||||
const lastKey = keys.pop();
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current[key] === null || current[key] === undefined) {
|
||||
return;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
if (lastKey && current.hasOwnProperty(lastKey)) {
|
||||
delete current[lastKey];
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete property ${path}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否有指定属性
|
||||
* @param obj 对象
|
||||
* @param path 属性路径,支持点号分隔的嵌套属性
|
||||
* @returns 是否有该属性
|
||||
*/
|
||||
hasProperty(obj: any, path: string): boolean {
|
||||
if (!obj || !path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (
|
||||
current === null ||
|
||||
current === undefined ||
|
||||
!current.hasOwnProperty(key)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有属性名
|
||||
* @param obj 对象
|
||||
* @returns 属性名数组
|
||||
*/
|
||||
getPropertyNames(obj: any): string[] {
|
||||
return this.reflectUtil.getFieldNames(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有方法名
|
||||
* @param obj 对象
|
||||
* @returns 方法名数组
|
||||
*/
|
||||
getMethodNames(obj: any): string[] {
|
||||
return this.reflectUtil.getMethodNames(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并对象
|
||||
* @param target 目标对象
|
||||
* @param sources 源对象数组
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
merge<T = any>(target: T, ...sources: any[]): T {
|
||||
if (!target) {
|
||||
target = {} as T;
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
if (source && typeof source === 'object') {
|
||||
this.copyProperties(source, target);
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并对象
|
||||
* @param target 目标对象
|
||||
* @param sources 源对象数组
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
deepMerge<T = any>(target: T, ...sources: any[]): T {
|
||||
if (!target) {
|
||||
target = {} as T;
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
if (source && typeof source === 'object') {
|
||||
this.deepMergeObject(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并单个对象
|
||||
* @param target 目标对象
|
||||
* @param source 源对象
|
||||
*/
|
||||
private deepMergeObject(target: any, source: any): void {
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = target[key];
|
||||
|
||||
if (this.isObject(sourceValue) && this.isObject(targetValue)) {
|
||||
this.deepMergeObject(targetValue, sourceValue);
|
||||
} else {
|
||||
target[key] = JSON.parse(JSON.stringify(sourceValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为对象
|
||||
* @param obj 要检查的值
|
||||
* @returns 是否为对象
|
||||
*/
|
||||
private isObject(obj: any): boolean {
|
||||
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象转数组
|
||||
* @param obj 对象
|
||||
* @returns 数组
|
||||
*/
|
||||
toArray(obj: any): any[] {
|
||||
if (!obj) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
return Object.values(obj);
|
||||
}
|
||||
|
||||
return [obj];
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组转对象
|
||||
* @param array 数组
|
||||
* @param keyField 作为键的字段名
|
||||
* @returns 对象
|
||||
*/
|
||||
toObject(array: any[], keyField: string): Record<string, any> {
|
||||
if (!array || !Array.isArray(array)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const obj: Record<string, any> = {};
|
||||
for (const item of array) {
|
||||
if (item && typeof item === 'object' && item[keyField] !== undefined) {
|
||||
obj[item[keyField]] = item;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤对象属性
|
||||
* @param obj 对象
|
||||
* @param predicate 过滤条件函数
|
||||
* @returns 过滤后的对象
|
||||
*/
|
||||
filterProperties(
|
||||
obj: any,
|
||||
predicate: (key: string, value: any) => boolean,
|
||||
): any {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const filtered: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key) && predicate(key, obj[key])) {
|
||||
filtered[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射对象属性
|
||||
* @param obj 对象
|
||||
* @param mapper 映射函数
|
||||
* @returns 映射后的对象
|
||||
*/
|
||||
mapProperties(obj: any, mapper: (key: string, value: any) => any): any {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapped: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
mapped[key] = mapper(key, obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
256
src/common/utils/reflect.util.ts
Normal file
256
src/common/utils/reflect.util.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 反射工具类
|
||||
* 基于 NestJS 实现 Java 风格的反射工具
|
||||
* 对应 Java: ReflectCallConstructor, ReflectCallMethod
|
||||
*/
|
||||
@Injectable()
|
||||
export class ReflectUtil {
|
||||
/**
|
||||
* 根据类名获取类
|
||||
* @param className 类名
|
||||
* @returns 类构造函数
|
||||
*/
|
||||
getClass<T = any>(className: string): new (...args: any[]) => T {
|
||||
try {
|
||||
// 在 Node.js 环境中,需要通过全局对象获取类
|
||||
const ClassConstructor = (global as any)[className];
|
||||
if (ClassConstructor && typeof ClassConstructor === 'function') {
|
||||
return ClassConstructor as new (...args: any[]) => T;
|
||||
}
|
||||
throw new Error(`Class ${className} not found`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get class ${className}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类名创建实例
|
||||
* @param className 类名
|
||||
* @returns 类实例
|
||||
*/
|
||||
newInstance<T = any>(className: string): T {
|
||||
try {
|
||||
const ClassConstructor = this.getClass<T>(className);
|
||||
return new ClassConstructor();
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to create instance of ${className}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类名和参数创建实例
|
||||
* @param className 类名
|
||||
* @param args 构造参数
|
||||
* @returns 类实例
|
||||
*/
|
||||
newInstanceWithArgs<T = any>(className: string, ...args: any[]): T {
|
||||
try {
|
||||
const ClassConstructor = this.getClass<T>(className);
|
||||
return new ClassConstructor(...args);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to create instance of ${className} with args: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用静态方法
|
||||
* @param className 类名
|
||||
* @param methodName 方法名
|
||||
* @param args 方法参数
|
||||
* @returns 方法返回值
|
||||
*/
|
||||
invokeStaticMethod<T = any>(
|
||||
className: string,
|
||||
methodName: string,
|
||||
...args: any[]
|
||||
): T {
|
||||
try {
|
||||
const ClassConstructor = this.getClass(className);
|
||||
if (typeof ClassConstructor[methodName] === 'function') {
|
||||
return ClassConstructor[methodName](...args);
|
||||
}
|
||||
throw new Error(
|
||||
`Static method ${methodName} not found in class ${className}`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to invoke static method ${className}.${methodName}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用实例方法
|
||||
* @param instance 实例对象
|
||||
* @param methodName 方法名
|
||||
* @param args 方法参数
|
||||
* @returns 方法返回值
|
||||
*/
|
||||
invokeMethod<T = any>(instance: any, methodName: string, ...args: any[]): T {
|
||||
try {
|
||||
if (instance && typeof instance[methodName] === 'function') {
|
||||
return instance[methodName](...args);
|
||||
}
|
||||
throw new Error(`Method ${methodName} not found in instance`);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to invoke method ${methodName}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置对象属性值
|
||||
* @param target 目标对象
|
||||
* @param fieldName 属性名
|
||||
* @param value 属性值
|
||||
*/
|
||||
setFieldValue(target: any, fieldName: string, value: any): void {
|
||||
try {
|
||||
if (target && typeof target === 'object') {
|
||||
target[fieldName] = value;
|
||||
} else {
|
||||
throw new Error('Target is not a valid object');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set field ${fieldName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象属性值
|
||||
* @param target 目标对象
|
||||
* @param fieldName 属性名
|
||||
* @returns 属性值
|
||||
*/
|
||||
getFieldValue<T = any>(target: any, fieldName: string): T {
|
||||
try {
|
||||
if (target && typeof target === 'object') {
|
||||
return target[fieldName];
|
||||
}
|
||||
throw new Error('Target is not a valid object');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get field ${fieldName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否有指定方法
|
||||
* @param target 目标对象
|
||||
* @param methodName 方法名
|
||||
* @returns 是否有该方法
|
||||
*/
|
||||
hasMethod(target: any, methodName: string): boolean {
|
||||
return (
|
||||
target &&
|
||||
typeof target === 'object' &&
|
||||
typeof target[methodName] === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否有指定属性
|
||||
* @param target 目标对象
|
||||
* @param fieldName 属性名
|
||||
* @returns 是否有该属性
|
||||
*/
|
||||
hasField(target: any, fieldName: string): boolean {
|
||||
return target && typeof target === 'object' && fieldName in target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有方法名
|
||||
* @param target 目标对象
|
||||
* @returns 方法名数组
|
||||
*/
|
||||
getMethodNames(target: any): string[] {
|
||||
if (!target || typeof target !== 'object') {
|
||||
return [];
|
||||
}
|
||||
return Object.getOwnPropertyNames(target).filter(
|
||||
(name) => typeof target[name] === 'function',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有属性名
|
||||
* @param target 目标对象
|
||||
* @returns 属性名数组
|
||||
*/
|
||||
getFieldNames(target: any): string[] {
|
||||
if (!target || typeof target !== 'object') {
|
||||
return [];
|
||||
}
|
||||
return Object.getOwnPropertyNames(target).filter(
|
||||
(name) => typeof target[name] !== 'function',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制对象属性
|
||||
* @param source 源对象
|
||||
* @param target 目标对象
|
||||
* @param fields 要复制的属性名数组,不指定则复制所有属性
|
||||
*/
|
||||
copyFields(source: any, target: any, fields?: string[]): void {
|
||||
try {
|
||||
if (
|
||||
!source ||
|
||||
!target ||
|
||||
typeof source !== 'object' ||
|
||||
typeof target !== 'object'
|
||||
) {
|
||||
throw new Error('Source and target must be valid objects');
|
||||
}
|
||||
|
||||
const fieldsToCopy = fields || Object.getOwnPropertyNames(source);
|
||||
fieldsToCopy.forEach((fieldName) => {
|
||||
if (source.hasOwnProperty(fieldName)) {
|
||||
target[fieldName] = source[fieldName];
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy fields: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否为空
|
||||
* @param obj 要检查的对象
|
||||
* @returns 是否为空
|
||||
*/
|
||||
isEmpty(obj: any): boolean {
|
||||
if (obj === null || obj === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return obj.length === 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.length === 0;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
return Object.keys(obj).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否不为空
|
||||
* @param obj 要检查的对象
|
||||
* @returns 是否不为空
|
||||
*/
|
||||
isNotEmpty(obj: any): boolean {
|
||||
return !this.isEmpty(obj);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user