@meta-1/nest-common
v0.0.39
Published
Common utilities and decorators for NestJS applications including caching, distributed lock, i18n, error handling, and more
Maintainers
Readme
@meta-1/nest-common
NestJS 应用的通用工具库,提供缓存、分布式锁、国际化、错误处理、事务管理等功能。
✨ 功能特性
- 🎯 缓存装饰器 - 类似 Spring Boot 的
@Cacheable和@CacheEvict装饰器,支持 Redis,CacheEvict支持keys和#{result}占位符 - 🔒 分布式锁 -
@WithLock装饰器,基于 Redis 实现分布式锁 - ❄️ 雪花ID生成器 -
@SnowflakeId装饰器,自动生成分布式唯一ID - 🔄 事务管理 -
@Transactional装饰器,自动管理数据库事务 - 🌍 国际化支持 -
@I18n装饰器和I18nContext,支持命名空间和自动采集 - ⚡ 响应拦截器 - 统一的 API 响应格式
- 🚨 错误处理 - 全局错误过滤器,支持自定义错误码
- 📄 分页DTO - 标准化的分页请求和响应 DTO
- 🌐 HTTP服务 - 增强的 HTTP 客户端,支持重试和文件下载
- ⚙️ 配置加载器 - 支持从本地 YAML 文件或 Nacos 配置中心加载配置
- ✅ 验证工具 -
createI18nZodDto自动采集验证错误消息 - 📚 Swagger工具 - 分页响应的 Swagger Schema 生成工具
- 🔤 I18n 收集器 - 开发环境自动收集缺失翻译键
- 📂 Locales 同步 -
syncLocales同步翻译文件到 i18n 目录 - 🗄️ 迁移 CLI -
runMigration可配置的 TypeORM 多模块迁移
📦 安装
npm install @meta-1/nest-common
# 或
pnpm add @meta-1/nest-common
# 或
yarn add @meta-1/nest-commonpeer 依赖
npm install @nestjs/common @nestjs/platform-express @nestjs-modules/ioredis nestjs-i18n nestjs-zod ioredis lodash nacos yaml zod说明: 缓存和分布式锁依赖 @nestjs-modules/ioredis,需先配置 RedisModule。
🚀 使用指南
1. 模块导入
import { CommonModule } from '@meta-1/nest-common';
@Module({
imports: [CommonModule],
})
export class AppModule {}CommonModule 会自动注册以下全局功能:
ResponseInterceptor- 统一响应格式ErrorsFilter- 全局错误处理ZodValidationPipe- Zod 验证管道HttpService- HTTP 客户端服务
2. 缓存装饰器
初始化
缓存功能依赖 @nestjs-modules/ioredis,需在应用模块中导入 RedisModule:
import { RedisModule } from '@nestjs-modules/ioredis';
@Module({
imports: [
RedisModule.forRoot({
type: 'single',
url: 'redis://localhost:6379',
}),
CommonModule,
],
})
export class AppModule {}CommonModule 内置 CacheableInitializer,会自动发现所有使用 @CacheableService() 的类并注入 Redis。
使用示例
import { CacheableService, Cacheable, CacheEvict } from '@meta-1/nest-common';
@CacheableService()
@Injectable()
export class UserService {
// 缓存结果,默认 TTL 300 秒
@Cacheable({ key: 'user:#{0}', ttl: 300 })
async getUserById(id: string) {
return await this.userRepository.findOne({ where: { id } });
}
// 使用对象属性作为缓存键
@Cacheable({ key: 'user:#{user.id}:profile', ttl: 600 })
async getUserProfile(user: { id: string }) {
return await this.userRepository.findProfile(user.id);
}
// 清除单个缓存
@CacheEvict({ key: 'user:#{0}' })
async updateUser(id: string, data: UpdateUserDto) {
return await this.userRepository.update(id, data);
}
// 清除多个缓存(支持从返回值读取)
@CacheEvict({
keys: ['user:#{0}', 'user:#{result.id}:profile', 'user:#{result.channel}:#{result.username}'],
})
async updateUserWithEvict(id: string, data: UpdateUserDto) {
const user = await this.userRepository.update(id, data);
return user; // #{result.id} 等从返回值读取
}
}缓存键占位符:
#{0},#{1},#{2}- 使用参数位置索引#{user.id},#{name}- 使用第一个参数的属性(等同于#{0.user.id})#{1.book.title}- 使用指定参数的路径属性#{result},#{result.id}- 从方法返回值读取(仅@CacheEvict的keys选项)
缓存键前缀: 键名会自动添加 cache: 前缀(若已以 cache: 开头则不会重复添加)
3. 分布式锁装饰器
初始化
分布式锁同样依赖 RedisModule,与缓存共用同一 Redis 实例。确保已按「缓存装饰器」章节配置 RedisModule 即可,LockInitializer 会自动发现并注入。
锁键前缀: 键名会自动添加 lock: 前缀(若已以 lock: 开头则不会重复添加)
使用示例
import { WithLock } from '@meta-1/nest-common';
@Injectable()
export class OrderService {
// 防止同一用户重复创建订单
@WithLock({
key: 'order:create:#{0}',
ttl: 10000, // 锁过期时间:10秒
waitTimeout: 3000, // 等待锁的超时时间:3秒
})
async createOrder(userId: string, items: OrderItem[]) {
// 同一用户的订单创建操作会被加锁
return await this.orderRepository.save({ userId, items });
}
// 防止重复支付
@WithLock({
key: 'payment:#{0}',
ttl: 30000,
waitTimeout: 0, // 不等待,立即失败
errorMessage: '订单正在支付中,请勿重复提交',
})
async processPayment(orderId: string) {
// 支付逻辑
}
// 使用对象属性作为锁键
@WithLock({
key: 'inventory:#{product.id}',
ttl: 5000,
})
async reduceInventory(product: { id: string; quantity: number }) {
// 库存扣减逻辑
}
}配置选项:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| key | string | 必填 | 锁的键名,支持占位符 |
| ttl | number | 30000 | 锁的过期时间(毫秒) |
| waitTimeout | number | 5000 | 等待锁的超时时间(毫秒),0 表示不等待 |
| retryInterval | number | 100 | 重试获取锁的间隔(毫秒) |
| errorMessage | string | '操作正在处理中,请稍后重试' | 获取锁失败时的错误提示 |
4. 雪花ID生成器
import { SnowflakeId } from '@meta-1/nest-common';
import { Entity, Column } from 'typeorm';
@Entity()
export class User {
@SnowflakeId()
id: string; // 自动生成分布式唯一ID
@Column()
name: string;
}
// 使用时无需手动设置 ID
const user = new User();
user.name = 'Alice';
await repository.save(user); // ID 自动生成环境变量配置:
SNOWFLAKE_WORKER_ID=0 # 0-31
SNOWFLAKE_DATACENTER_ID=0 # 0-31特性:
- 并发安全:通过 Promise 链确保多实例同时插入时 ID 不重复
- 分布式唯一:支持多机部署
- 时间有序:ID 按生成时间递增
- 高性能:单机每毫秒可生成 4096 个唯一 ID
- 时间回拨保护:检测并拒绝时钟回拨
- 自定义起始时间:epoch 为 2021-01-01 00:00:00 UTC
5. 事务装饰器
import { Transactional } from '@meta-1/nest-common';
@Injectable()
export class OrderService {
constructor(
@InjectRepository(Order) private orderRepo: Repository<Order>,
@InjectRepository(OrderItem) private itemRepo: Repository<OrderItem>,
) {}
@Transactional()
async createOrder(orderData: CreateOrderDto) {
// 所有数据库操作都在同一个事务中
const order = await this.orderRepo.save(orderData);
for (const item of orderData.items) {
await this.itemRepo.save({ ...item, orderId: order.id });
}
// 如果任何操作失败,整个事务会自动回滚
return order;
}
}注意事项:
- Service 中必须注入至少一个 Repository
- 方法内抛出的任何错误都会触发回滚
- 只有方法正常返回时事务才会提交
6. 国际化支持
设置
import { I18nModule } from 'nestjs-i18n';
import * as path from 'path';
@Module({
imports: [
I18nModule.forRoot({
fallbackLanguage: 'en',
loaderOptions: {
path: path.join(__dirname, '/i18n/'),
watch: true,
},
}),
],
})
export class AppModule {}使用示例
import { I18n, I18nContext } from '@meta-1/nest-common';
@Controller('users')
export class UserController {
@Get()
async getUsers(@I18n() i18n: I18nContext) {
const users = await this.userService.findAll();
return {
message: i18n.t('users.list.success'), // 自动添加 'common.' 前缀
data: users,
};
}
@Post()
async createUser(
@Body() dto: CreateUserDto,
@I18n() i18n: I18nContext,
) {
const user = await this.userService.create(dto);
return {
message: i18n.t('users.create.success', {
args: { name: user.name },
}),
data: user,
};
}
}自定义命名空间:
import { createI18nContext, type RawI18nContext } from '@meta-1/nest-common';
import { I18n as NestI18n } from 'nestjs-i18n';
@Controller('products')
export class ProductController {
@Get()
async getProducts(@NestI18n() rawI18n: RawI18nContext) {
const i18n = createI18nContext(rawI18n, 'products');
return {
message: i18n.t('list.success'), // 翻译为 'products.list.success'
data: await this.productService.findAll(),
};
}
}7. 错误处理
定义错误码
import { defineErrorCode, AppError } from '@meta-1/nest-common';
// 定义模块错误码
export const UserErrorCode = defineErrorCode({
USER_NOT_FOUND: { code: 2000, message: 'User not found' },
USER_ALREADY_EXISTS: { code: 2001, message: 'User already exists' },
});
// 使用
@Injectable()
export class UserService {
async getUserById(id: string) {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new AppError(UserErrorCode.USER_NOT_FOUND, { userId: id });
}
return user;
}
}错误码范围约定:
0-999: 通用错误(@meta-1/nest-common)1000-1999: Message 模块错误2000-2999: User 模块错误3000-3999: Auth 模块错误100-199: 分布式锁错误
错误响应格式:
{
"code": 2000,
"success": false,
"message": "User not found",
"data": { "userId": "123" },
"timestamp": "2024-01-01T00:00:00.000Z",
"path": "/api/users/123"
}8. 分页DTO
import { PageRequestDto, PageDataDto } from '@meta-1/nest-common';
import { ApiOkResponse } from '@nestjs/swagger';
import { createPageSchema, createPageModels } from '@meta-1/nest-common';
@Controller('users')
export class UserController {
@Get()
@ApiOkResponse({
schema: createPageSchema(UserDto),
})
@ApiExtraModels(...createPageModels(UserDto))
async getUsers(@Query() query: PageRequestDto) {
const [data, total] = await this.userService.findAndCount(query);
return PageDataDto.of(total, data);
}
}9. HTTP服务
import { HttpService } from '@meta-1/nest-common';
@Injectable()
export class ExternalApiService {
constructor(private readonly httpService: HttpService) {}
async fetchData() {
// GET 请求
const response = await this.httpService.get<Data>('https://api.example.com/data');
return response.data;
}
async postData(data: any) {
// POST 请求(支持重试)
const response = await this.httpService.post<Result>('https://api.example.com/data', data, {
retries: 3,
retryDelay: 1000,
});
return response.data;
}
async downloadFile(url: string, filePath: string) {
// 下载文件(支持进度回调)
await this.httpService.download({
url,
filePath,
onProgress: (progress) => {
console.log(`下载进度: ${progress}%`);
},
});
}
}10. 配置加载器
import { ConfigLoader, ConfigSourceType } from '@meta-1/nest-common';
// 从本地 YAML 文件加载
const loader = new ConfigLoader<AppConfig>({
type: ConfigSourceType.LOCAL_YAML,
filePath: './config/app.yaml',
});
const config = await loader.load();
// 从 Nacos 加载
const nacosLoader = new ConfigLoader<AppConfig>({
type: ConfigSourceType.NACOS,
server: '127.0.0.1:8848',
dataId: 'app-config',
group: 'DEFAULT_GROUP',
namespace: 'public',
username: 'nacos',
password: 'nacos',
});
const nacosConfig = await nacosLoader.load();配置特性:
- 自动将 kebab-case 键名转换为 camelCase
- 支持 YAML 格式
- 支持 Nacos 配置中心
11. 验证工具
import { createI18nZodDto } from '@meta-1/nest-common';
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export class CreateUserDto extends createI18nZodDto(CreateUserSchema) {}特性:
- 自动采集 Schema 中的所有验证错误消息到 i18n collector
- 支持开发环境自动收集翻译 key
12. Swagger工具
import { createPageSchema, createPageModels } from '@meta-1/nest-common';
import { ApiOkResponse, ApiExtraModels } from '@nestjs/swagger';
@Controller('users')
export class UserController {
@Get()
@ApiOkResponse({
schema: createPageSchema(UserDto),
})
@ApiExtraModels(...createPageModels(UserDto))
async getUsers() {
// ...
}
}13. 工具函数
import { generateKey, md5, PlainTextLogger } from '@meta-1/nest-common';
// 生成动态键名(参数)
const key = generateKey('user:#{0}:profile:#{1.name}', ['123', { name: 'Alice' }]);
// 结果: 'user:123:profile:Alice'
// 从返回值生成键名(用于 CacheEvict 的 keys)
const evictKey = generateKey('user:#{result.id}', [], { id: '456' });
// 结果: 'user:456'
// MD5 哈希
const hash = md5('hello world');
// TypeORM 纯文本日志输出器
const logger = new PlainTextLogger();14. I18n 缺失键收集器
开发环境下自动收集缺失的翻译键并写入 locales 文件:
import * as path from 'path';
import { initI18nCollector, getI18nCollector } from '@meta-1/nest-common';
import { I18nModule } from 'nestjs-i18n';
// 1. 在应用启动时先初始化收集器(仅开发环境)
if (process.env.NODE_ENV === 'development') {
const localesDir = path.join(process.cwd(), 'locales');
initI18nCollector(localesDir);
}
// 2. 配置 I18nModule 时传入 missingKeyHandler
I18nModule.forRoot({
// ... 其他配置
missingKeyHandler: (key: string) => {
const collector = getI18nCollector();
if (collector) {
const actualKey = key.includes('.') ? key.split('.').slice(1).join('.') : key;
collector.add(actualKey);
}
},
});15. Locales 同步工具
将 locales/*.json 同步到 i18n 目标目录:
import { syncLocales } from '@meta-1/nest-common';
// 一次性同步
syncLocales({
sourceDir: './locales',
targetDir: './dist/apps/server/i18n',
});
// 监听模式(开发时)
syncLocales({
sourceDir: './locales',
targetDir: './dist/apps/server/i18n',
watch: true,
});16. TypeORM 迁移 CLI
可配置的迁移脚本,支持多模块:
import { runMigration } from '@meta-1/nest-common';
runMigration({
libsPath: path.join(process.cwd(), 'libs'),
dataSourcePath: path.join(process.cwd(), 'apps/server/src/data-source.ts'),
});用法:
migration:generate <模块名> <迁移名称>- 生成迁移migration:create <模块名> <迁移名称>- 创建空迁移migration:run- 执行所有待运行迁移migration:revert- 回滚最后一次迁移migration:show- 显示迁移状态
📝 API 参考
装饰器
@CacheableService()- 标记服务类支持缓存@Cacheable(options)- 缓存方法结果@CacheEvict(options)- 清除缓存@WithLock(options)- 分布式锁@SnowflakeId()- 自动生成雪花ID@Transactional()- 自动事务管理@I18n()- 注入 I18nContext
类
CommonModule- 通用模块AppError- 自定义错误类I18nContext- 国际化上下文ErrorsFilter- 全局异常过滤器ResponseInterceptor- 响应拦截器HttpService- HTTP 客户端服务PageRequestDto- 分页请求 DTOPageDataDto<T>- 分页响应 DTOConfigLoader<T>- 配置加载器PlainTextLogger- TypeORM 纯文本日志输出器
函数
defineErrorCode(definition)- 定义错误码createI18nZodDto(schema)- 创建支持 i18n 的 Zod DTOcreateI18nContext(context, namespace)- 创建自定义命名空间上下文createPageSchema(itemDto)- 创建分页 Swagger SchemacreatePageModels(itemDto)- 创建分页 Swagger ModelsgenerateKey(pattern, args, result?)- 生成动态键名,result用于#{result}占位符md5(text)- MD5 哈希initI18nCollector(localesDir)- 初始化 I18n 缺失键收集器(仅开发环境)getI18nCollector()- 获取 I18n 收集器实例syncLocales(options)- 同步 locales 文件到 i18n 目录runMigration(config)- 执行 TypeORM 迁移 CLI
错误码
通用错误码 (0-999):
SERVER_ERROR(500) - 服务器错误VALIDATION_FAILED(400) - 验证失败UNAUTHORIZED(401) - 未授权FORBIDDEN(403) - 禁止访问NOT_FOUND(404) - 未找到I18N_CONTEXT_NOT_FOUND(500) - I18n 上下文未找到CONFIG_NOT_FOUND(3000) - 配置未找到CONFIG_INVALID(3001) - 配置无效
分布式锁错误码 (100-199):
REDIS_NOT_INJECTED(100) - Redis 未注入LOCK_ACQUIRE_FAILED(110) - 获取锁失败LOCK_ACQUIRE_ERROR(111) - 获取锁时发生错误LOCK_RELEASE_ERROR(112) - 释放锁时发生错误
📄 License
MIT
