chore: sync changes for v0.1.1
This commit is contained in:
@@ -1,384 +0,0 @@
|
||||
# 队列系统设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
本队列系统实现了事件与任务分离的设计理念:
|
||||
- **事件(Events)**: 走 Kafka 或 Outbox→Kafka,用于领域事件的发布和订阅
|
||||
- **任务(Tasks)**: 走 Redis 或 Outbox→Worker,用于异步任务的处理
|
||||
- **Outbox 模式**: 支持数据库作为 Outbox,确保事务一致性
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ 业务服务 │ │ 统一队列服务 │
|
||||
│ │────│ │
|
||||
│ - 用户服务 │ │ UnifiedQueue │
|
||||
│ - 订单服务 │ │ Service │
|
||||
│ - 支付服务 │ └─────────────────┘
|
||||
└─────────────────┘ │
|
||||
│
|
||||
┌───────────────────────┼───────────────────────┐
|
||||
│ │ │
|
||||
┌───────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐
|
||||
│ 任务队列提供者 │ │ 事件总线提供者 │ │ 队列工厂服务 │
|
||||
│ │ │ │ │ │
|
||||
│ ITaskQueue │ │ IEventBus │ │ QueueFactory │
|
||||
│ Provider │ │ Provider │ │ Service │
|
||||
└────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
┌───────┼───────┐ ┌────────┼────────┐ │
|
||||
│ │ │ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼ ▼ ▼
|
||||
Redis Database Memory Kafka Database Memory 配置管理
|
||||
Queue Outbox Queue Events Outbox Events
|
||||
```
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. 接口定义 (`queue.interface.ts`)
|
||||
|
||||
```typescript
|
||||
// 任务队列接口
|
||||
export abstract class ITaskQueueProvider {
|
||||
abstract addTask<T>(queueName: string, taskName: string, data: T, options?: TaskJobOptions): Promise<TaskJob<T>>;
|
||||
abstract processTask<T>(queueName: string, taskName: string, processor: TaskProcessor<T>): Promise<void>;
|
||||
abstract getStats(queueName: string): Promise<any>;
|
||||
abstract clean(queueName: string, grace: number): Promise<void>;
|
||||
abstract pause(queueName: string): Promise<void>;
|
||||
abstract resume(queueName: string): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
}
|
||||
|
||||
// 事件总线接口
|
||||
export abstract class IEventBusProvider {
|
||||
abstract publish<T>(event: DomainEvent<T>, options?: EventPublishOptions): Promise<void>;
|
||||
abstract subscribe<T>(eventType: string, handler: EventHandler<T>, options?: any): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实现提供者
|
||||
|
||||
#### Redis 任务队列 (`redis-task-queue.provider.ts`)
|
||||
- 基于 BullMQ 实现
|
||||
- 支持任务重试、延迟执行、优先级
|
||||
- 提供任务统计和管理功能
|
||||
|
||||
#### Kafka 事件总线 (`kafka-event-bus.provider.ts`)
|
||||
- 基于 KafkaJS 实现
|
||||
- 支持事件发布和订阅
|
||||
- 提供消费者组管理
|
||||
|
||||
#### 数据库 Outbox (`database-queue.provider.ts`)
|
||||
- 同时实现任务队列和事件总线接口
|
||||
- 基于数据库事务确保一致性
|
||||
- 支持定时轮询处理
|
||||
|
||||
### 3. 队列工厂 (`queue-factory.service.ts`)
|
||||
- 根据配置动态创建提供者实例
|
||||
- 支持运行时切换适配器
|
||||
- 提供健康检查功能
|
||||
|
||||
### 4. 统一服务 (`unified-queue.service.ts`)
|
||||
- 提供统一的任务和事件操作接口
|
||||
- 封装常用业务场景的便捷方法
|
||||
- 支持批量操作
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
# 适配器选择
|
||||
TASK_QUEUE_ADAPTER=redis # redis, database-outbox, memory
|
||||
EVENT_BUS_ADAPTER=kafka # kafka, database-outbox, memory
|
||||
|
||||
# Redis 配置(任务队列)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# Kafka 配置(事件总线)
|
||||
KAFKA_CLIENT_ID=wwjcloud
|
||||
KAFKA_BROKERS=localhost:9092
|
||||
KAFKA_GROUP_ID=wwjcloud-group
|
||||
KAFKA_TOPIC_PREFIX=domain-events
|
||||
|
||||
# 队列配置
|
||||
QUEUE_REMOVE_ON_COMPLETE=100
|
||||
QUEUE_REMOVE_ON_FAIL=50
|
||||
QUEUE_DEFAULT_ATTEMPTS=3
|
||||
QUEUE_BACKOFF_DELAY=2000
|
||||
|
||||
# Outbox 配置
|
||||
OUTBOX_PROCESS_INTERVAL=5000 # 处理间隔(毫秒)
|
||||
OUTBOX_BATCH_SIZE=100 # 批处理大小
|
||||
OUTBOX_MAX_RETRIES=5 # 最大重试次数
|
||||
OUTBOX_RETRY_DELAY=60000 # 重试延迟(毫秒)
|
||||
```
|
||||
|
||||
### 配置文件 (`src/config/queue/index.ts`)
|
||||
|
||||
```typescript
|
||||
export const queueConfig = () => ({
|
||||
// 适配器配置
|
||||
taskAdapter: process.env.TASK_QUEUE_ADAPTER || 'database-outbox',
|
||||
eventAdapter: process.env.EVENT_BUS_ADAPTER || 'database-outbox',
|
||||
|
||||
// Redis 任务队列配置
|
||||
removeOnComplete: parseInt(process.env.QUEUE_REMOVE_ON_COMPLETE || '100'),
|
||||
removeOnFail: parseInt(process.env.QUEUE_REMOVE_ON_FAIL || '50'),
|
||||
defaultAttempts: parseInt(process.env.QUEUE_DEFAULT_ATTEMPTS || '3'),
|
||||
backoffDelay: parseInt(process.env.QUEUE_BACKOFF_DELAY || '2000'),
|
||||
|
||||
// Outbox 模式配置
|
||||
outboxProcessInterval: parseInt(process.env.OUTBOX_PROCESS_INTERVAL || '5000'),
|
||||
outboxBatchSize: parseInt(process.env.OUTBOX_BATCH_SIZE || '100'),
|
||||
outboxMaxRetries: parseInt(process.env.OUTBOX_MAX_RETRIES || '5'),
|
||||
outboxRetryDelay: parseInt(process.env.OUTBOX_RETRY_DELAY || '60000'),
|
||||
});
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 基本使用
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UnifiedQueueService } from '@/core/queue/unified-queue.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly queueService: UnifiedQueueService,
|
||||
) {}
|
||||
|
||||
async registerUser(userData: any) {
|
||||
// 1. 发布用户注册事件
|
||||
await this.queueService.publishUserEvent('registered', userData.id, {
|
||||
email: userData.email,
|
||||
name: userData.name,
|
||||
});
|
||||
|
||||
// 2. 添加发送欢迎邮件任务
|
||||
await this.queueService.sendEmail(
|
||||
userData.email,
|
||||
'欢迎注册',
|
||||
`欢迎 ${userData.name}!`,
|
||||
{ delay: 5000 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 注册处理器
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class QueueProcessorService implements OnModuleInit {
|
||||
constructor(
|
||||
private readonly queueService: UnifiedQueueService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
// 注册任务处理器
|
||||
await this.queueService.processTask('email', 'send', async (job) => {
|
||||
console.log('发送邮件:', job.data);
|
||||
// 实际邮件发送逻辑
|
||||
});
|
||||
|
||||
// 注册事件处理器
|
||||
await this.queueService.subscribeEvent('user.registered', async (event) => {
|
||||
console.log('用户注册事件:', event);
|
||||
// 处理用户注册后的业务逻辑
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量发布事件
|
||||
const events = users.map(user => ({
|
||||
eventId: `user-update-${user.id}-${Date.now()}`,
|
||||
eventType: 'user.updated',
|
||||
aggregateId: user.id.toString(),
|
||||
aggregateType: 'User',
|
||||
data: user.changes,
|
||||
version: 1,
|
||||
occurredAt: Date.now(),
|
||||
}));
|
||||
|
||||
await this.queueService.publishEvents(events);
|
||||
```
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### jobs 表(任务)
|
||||
```sql
|
||||
CREATE TABLE `jobs` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`queue_name` varchar(255) NOT NULL COMMENT '队列名称',
|
||||
`job_name` varchar(255) NOT NULL COMMENT '任务名称',
|
||||
`payload` text NOT NULL COMMENT '任务数据',
|
||||
`attempts` int DEFAULT '0' COMMENT '尝试次数',
|
||||
`max_attempts` int DEFAULT '3' COMMENT '最大尝试次数',
|
||||
`available_at` int NOT NULL COMMENT '可执行时间',
|
||||
`created_at` int NOT NULL COMMENT '创建时间',
|
||||
`status` enum('pending','processing','completed','failed') DEFAULT 'pending',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_jobs_queue_status_available_at` (`queue_name`,`status`,`available_at`)
|
||||
);
|
||||
```
|
||||
|
||||
### events 表(事件)
|
||||
```sql
|
||||
CREATE TABLE `events` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`event_id` varchar(36) NOT NULL COMMENT '事件唯一标识',
|
||||
`event_type` varchar(255) NOT NULL COMMENT '事件类型',
|
||||
`aggregate_id` varchar(255) NOT NULL COMMENT '聚合根ID',
|
||||
`aggregate_type` varchar(255) NOT NULL COMMENT '聚合根类型',
|
||||
`event_data` text NOT NULL COMMENT '事件数据',
|
||||
`occurred_at` int NOT NULL COMMENT '发生时间',
|
||||
`processed_at` int DEFAULT '0' COMMENT '处理时间',
|
||||
`status` enum('pending','processing','processed','failed') DEFAULT 'pending',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_events_event_id` (`event_id`),
|
||||
KEY `idx_events_type_processed` (`event_type`,`processed_at`)
|
||||
);
|
||||
```
|
||||
|
||||
## 部署配置
|
||||
|
||||
### 1. 开发环境(使用 Database Outbox)
|
||||
```bash
|
||||
TASK_QUEUE_ADAPTER=database-outbox
|
||||
EVENT_BUS_ADAPTER=database-outbox
|
||||
```
|
||||
|
||||
### 2. 生产环境(使用 Redis + Kafka)
|
||||
```bash
|
||||
TASK_QUEUE_ADAPTER=redis
|
||||
EVENT_BUS_ADAPTER=kafka
|
||||
|
||||
REDIS_HOST=redis.example.com
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your-password
|
||||
|
||||
KAFKA_BROKERS=kafka1.example.com:9092,kafka2.example.com:9092
|
||||
KAFKA_GROUP_ID=wwjcloud-prod
|
||||
```
|
||||
|
||||
### 3. 混合环境(任务用 Redis,事件用 Database)
|
||||
```bash
|
||||
TASK_QUEUE_ADAPTER=redis
|
||||
EVENT_BUS_ADAPTER=database-outbox
|
||||
```
|
||||
|
||||
## 监控和运维
|
||||
|
||||
### 1. 健康检查
|
||||
```typescript
|
||||
const health = await queueService.healthCheck();
|
||||
console.log(health);
|
||||
// {
|
||||
// taskQueue: { status: 'healthy', details: {...} },
|
||||
// eventBus: { status: 'healthy', details: {...} }
|
||||
// }
|
||||
```
|
||||
|
||||
### 2. 队列统计
|
||||
```typescript
|
||||
const stats = await queueService.getTaskQueueStats('email');
|
||||
console.log(stats);
|
||||
// {
|
||||
// waiting: 10,
|
||||
// active: 2,
|
||||
// completed: 100,
|
||||
// failed: 5
|
||||
// }
|
||||
```
|
||||
|
||||
### 3. 队列管理
|
||||
```typescript
|
||||
// 暂停队列
|
||||
await queueService.pauseTaskQueue('email');
|
||||
|
||||
// 恢复队列
|
||||
await queueService.resumeTaskQueue('email');
|
||||
|
||||
// 清理已完成任务
|
||||
await queueService.cleanTaskQueue('email', 3600000); // 保留1小时
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 事件设计
|
||||
- 事件应该是过去时态,描述已经发生的事情
|
||||
- 事件数据应该包含足够的上下文信息
|
||||
- 使用版本控制来处理事件结构变化
|
||||
|
||||
### 2. 任务设计
|
||||
- 任务应该是幂等的,可以安全重试
|
||||
- 任务数据应该包含所有必要的信息
|
||||
- 合理设置重试次数和延迟时间
|
||||
|
||||
### 3. 错误处理
|
||||
- 实现适当的错误处理和重试机制
|
||||
- 记录详细的错误日志
|
||||
- 设置死信队列处理失败任务
|
||||
|
||||
### 4. 性能优化
|
||||
- 合理设置批处理大小
|
||||
- 使用适当的并发数
|
||||
- 定期清理已完成的任务和事件
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 1. 任务不执行
|
||||
- 检查队列配置是否正确
|
||||
- 确认任务处理器已注册
|
||||
- 查看任务状态和错误日志
|
||||
|
||||
### 2. 事件丢失
|
||||
- 检查事件总线连接状态
|
||||
- 确认事件处理器已注册
|
||||
- 查看事件表中的处理状态
|
||||
|
||||
### 3. 性能问题
|
||||
- 监控队列长度和处理速度
|
||||
- 检查数据库连接池配置
|
||||
- 优化任务和事件处理逻辑
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 1. 添加新的适配器
|
||||
1. 实现 `ITaskQueueProvider` 或 `IEventBusProvider` 接口
|
||||
2. 在 `QueueFactoryService` 中添加创建逻辑
|
||||
3. 更新配置和文档
|
||||
|
||||
### 2. 自定义任务类型
|
||||
1. 定义任务数据结构
|
||||
2. 实现任务处理器
|
||||
3. 在统一服务中添加便捷方法
|
||||
|
||||
### 3. 监控集成
|
||||
1. 添加指标收集
|
||||
2. 集成监控系统
|
||||
3. 设置告警规则
|
||||
|
||||
## 源码仓库
|
||||
|
||||
项目托管在 Gitee 上:https://gitee.com/your-org/wwjcloud-nestjs
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 项目到你的 Gitee 账户
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交你的修改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
@@ -1,245 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UnifiedQueueService } from '../src/core/queue/unifiedQueueService';
|
||||
import { TaskJobOptions, EventPublishOptions } from '../src/core/interfaces/queue.interface';
|
||||
|
||||
/**
|
||||
* 队列使用示例
|
||||
* 演示如何使用新的队列系统
|
||||
*/
|
||||
@Injectable()
|
||||
export class QueueUsageExample {
|
||||
constructor(
|
||||
private readonly queueService: UnifiedQueueService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 示例:用户注册流程
|
||||
*/
|
||||
async handleUserRegistration(userData: {
|
||||
id: number;
|
||||
email: string;
|
||||
phone: string;
|
||||
name: string;
|
||||
}) {
|
||||
// 1. 发布用户注册事件(事件总线)
|
||||
await this.queueService.publishUserEvent('registered', userData.id, {
|
||||
email: userData.email,
|
||||
phone: userData.phone,
|
||||
name: userData.name,
|
||||
registeredAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 2. 添加发送欢迎邮件任务(任务队列)
|
||||
await this.queueService.sendEmail(
|
||||
userData.email,
|
||||
'欢迎注册',
|
||||
`欢迎 ${userData.name} 注册我们的平台!`,
|
||||
{
|
||||
delay: 5000, // 5秒后发送
|
||||
attempts: 3,
|
||||
priority: 1,
|
||||
}
|
||||
);
|
||||
|
||||
// 3. 添加发送短信验证码任务(任务队列)
|
||||
await this.queueService.sendSms(
|
||||
userData.phone,
|
||||
'您的验证码是:123456',
|
||||
{
|
||||
attempts: 2,
|
||||
priority: 1,
|
||||
}
|
||||
);
|
||||
|
||||
// 4. 添加数据同步任务(任务队列)
|
||||
await this.queueService.syncData('user-profile', {
|
||||
userId: userData.id,
|
||||
action: 'create',
|
||||
data: userData,
|
||||
}, {
|
||||
delay: 10000, // 10秒后同步
|
||||
priority: 3,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例:订单处理流程
|
||||
*/
|
||||
async handleOrderCreated(orderData: {
|
||||
id: number;
|
||||
userId: number;
|
||||
amount: number;
|
||||
items: any[];
|
||||
}) {
|
||||
// 1. 发布订单创建事件(事件总线)
|
||||
await this.queueService.publishOrderEvent('created', orderData.id, {
|
||||
userId: orderData.userId,
|
||||
amount: orderData.amount,
|
||||
itemCount: orderData.items.length,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 2. 添加库存扣减任务(任务队列)
|
||||
await this.queueService.addTask('inventory', 'reduce', {
|
||||
orderId: orderData.id,
|
||||
items: orderData.items,
|
||||
}, {
|
||||
priority: 1,
|
||||
attempts: 5,
|
||||
});
|
||||
|
||||
// 3. 添加支付处理任务(任务队列)
|
||||
await this.queueService.addTask('payment', 'process', {
|
||||
orderId: orderData.id,
|
||||
amount: orderData.amount,
|
||||
userId: orderData.userId,
|
||||
}, {
|
||||
delay: 1000, // 1秒后处理
|
||||
priority: 1,
|
||||
attempts: 3,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例:批量事件发布
|
||||
*/
|
||||
async handleBatchUserUpdate(users: Array<{ id: number; changes: any }>) {
|
||||
const events = users.map(user => ({
|
||||
eventType: 'user.updated',
|
||||
aggregateId: user.id.toString(),
|
||||
tenantId: 'default',
|
||||
idempotencyKey: `user-update-${user.id}-${Date.now()}`,
|
||||
traceId: `trace-${Date.now()}`,
|
||||
data: user.changes,
|
||||
version: '1',
|
||||
occurredAt: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
// 批量发布事件
|
||||
await this.queueService.publishEvents(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例:注册任务处理器
|
||||
*/
|
||||
async registerTaskProcessors() {
|
||||
// 注册邮件发送处理器
|
||||
await this.queueService.processTask('email', async (job: any) => {
|
||||
console.log('处理邮件发送任务:', job.data);
|
||||
// 实际的邮件发送逻辑
|
||||
await this.sendEmailImplementation(job.data);
|
||||
});
|
||||
|
||||
// 注册短信发送处理器
|
||||
await this.queueService.processTask('sms', async (job: any) => {
|
||||
console.log('处理短信发送任务:', job.data);
|
||||
// 实际的短信发送逻辑
|
||||
await this.sendSmsImplementation(job.data);
|
||||
});
|
||||
|
||||
// 注册库存扣减处理器
|
||||
await this.queueService.processTask('inventory', async (job: any) => {
|
||||
console.log('处理库存扣减任务:', job.data);
|
||||
// 实际的库存扣减逻辑
|
||||
await this.reduceInventoryImplementation(job.data);
|
||||
});
|
||||
|
||||
// 注册支付处理器
|
||||
await this.queueService.processTask('payment', async (job: any) => {
|
||||
console.log('处理支付任务:', job.data);
|
||||
// 实际的支付处理逻辑
|
||||
await this.processPaymentImplementation(job.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例:注册事件处理器
|
||||
*/
|
||||
async registerEventHandlers() {
|
||||
// 注册用户注册事件处理器
|
||||
await this.queueService.subscribeEvent('user.registered', async (event: any) => {
|
||||
console.log('处理用户注册事件:', event);
|
||||
// 可以触发其他业务逻辑,如发送通知、更新统计等
|
||||
await this.handleUserRegisteredEvent(event);
|
||||
});
|
||||
|
||||
// 注册订单创建事件处理器
|
||||
await this.queueService.subscribeEvent('order.created', async (event: any) => {
|
||||
console.log('处理订单创建事件:', event);
|
||||
// 可以触发其他业务逻辑,如发送通知、更新报表等
|
||||
await this.handleOrderCreatedEvent(event);
|
||||
});
|
||||
|
||||
// 注册用户更新事件处理器
|
||||
await this.queueService.subscribeEvent('user.updated', async (event: any) => {
|
||||
console.log('处理用户更新事件:', event);
|
||||
// 可以触发缓存更新、搜索索引更新等
|
||||
await this.handleUserUpdatedEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例:监控和管理
|
||||
*/
|
||||
async monitorQueues() {
|
||||
// 获取任务队列统计
|
||||
const emailStats = await this.queueService.getTaskQueueStats('email');
|
||||
const smsStats = await this.queueService.getTaskQueueStats('sms');
|
||||
|
||||
console.log('邮件队列统计:', emailStats);
|
||||
console.log('短信队列统计:', smsStats);
|
||||
|
||||
// 健康检查
|
||||
const health = await this.queueService.healthCheck();
|
||||
console.log('队列健康状态:', health);
|
||||
|
||||
// 清理已完成的任务(保留最近1小时的)
|
||||
await this.queueService.cleanTaskQueue('email', 3600000);
|
||||
await this.queueService.cleanTaskQueue('sms', 3600000);
|
||||
}
|
||||
|
||||
// ==================== 私有实现方法 ====================
|
||||
|
||||
private async sendEmailImplementation(data: any) {
|
||||
// 实际的邮件发送实现
|
||||
console.log('发送邮件:', data);
|
||||
// 模拟异步操作
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
private async sendSmsImplementation(data: any) {
|
||||
// 实际的短信发送实现
|
||||
console.log('发送短信:', data);
|
||||
// 模拟异步操作
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
private async reduceInventoryImplementation(data: any) {
|
||||
// 实际的库存扣减实现
|
||||
console.log('扣减库存:', data);
|
||||
// 模拟异步操作
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
private async processPaymentImplementation(data: any) {
|
||||
// 实际的支付处理实现
|
||||
console.log('处理支付:', data);
|
||||
// 模拟异步操作
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
}
|
||||
|
||||
private async handleUserRegisteredEvent(event: any) {
|
||||
// 处理用户注册事件的业务逻辑
|
||||
console.log('用户注册事件处理:', event);
|
||||
}
|
||||
|
||||
private async handleOrderCreatedEvent(event: any) {
|
||||
// 处理订单创建事件的业务逻辑
|
||||
console.log('订单创建事件处理:', event);
|
||||
}
|
||||
|
||||
private async handleUserUpdatedEvent(event: any) {
|
||||
// 处理用户更新事件的业务逻辑
|
||||
console.log('用户更新事件处理:', event);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bull": "^4.16.5",
|
||||
"bullmq": "^5.7.0",
|
||||
"cache-manager": "^7.1.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
|
||||
@@ -11,27 +11,4 @@ export class AppController {
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('healthz')
|
||||
@Public()
|
||||
healthCheck() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('readyz')
|
||||
@Public()
|
||||
readinessCheck() {
|
||||
return {
|
||||
status: 'ready',
|
||||
timestamp: new Date().toISOString(),
|
||||
services: {
|
||||
redis: 'connected',
|
||||
kafka: 'connected',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,10 @@ import { ConfigModule } from './config';
|
||||
// 新增:全局异常过滤器、统一响应、健康
|
||||
import { HttpExceptionFilter } from './core/http/filters/httpExceptionFilter';
|
||||
import { ResponseInterceptor } from './core/http/interceptors/responseInterceptor';
|
||||
import { HealthController } from './core/observability/health/health.controller';
|
||||
import { HealthController as ObHealthController } from './core/observability/health/health.controller';
|
||||
import { HealthModule as K8sHealthModule } from './core/health/healthModule';
|
||||
import { HttpMetricsService } from './core/observability/metrics/httpMetricsService';
|
||||
import { OutboxKafkaForwarderModule } from './core/event/outboxKafkaForwarder.module';
|
||||
import { HealthAggregator } from './core/observability/health/health-aggregator';
|
||||
import { DbHealthIndicator } from './core/observability/health/indicators/db.indicator';
|
||||
import { RedisHealthIndicator } from './core/observability/health/indicators/redis.indicator';
|
||||
@@ -108,6 +110,8 @@ const dbImports =
|
||||
LOG_LEVEL: Joi.string(),
|
||||
THROTTLE_TTL: Joi.number(),
|
||||
THROTTLE_LIMIT: Joi.number(),
|
||||
STARTUP_HEALTH_CHECK: Joi.string().valid('true', 'false').optional(),
|
||||
STARTUP_HEALTH_TIMEOUT_MS: Joi.number().min(100).optional(),
|
||||
}),
|
||||
}),
|
||||
// 缓存(内存实现,后续可替换为 redis-store)
|
||||
@@ -137,45 +141,47 @@ const dbImports =
|
||||
],
|
||||
}),
|
||||
}),
|
||||
// 健康检查(需要时可增加控制器)
|
||||
// 健康检查(Terminus 聚合)
|
||||
TerminusModule,
|
||||
// K8s 探针端点
|
||||
K8sHealthModule,
|
||||
// 日志
|
||||
WinstonModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
level: config.get('logLevel'),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(({ level, message, timestamp, context }) => {
|
||||
try {
|
||||
const { ClsServiceManager } = require('nestjs-cls');
|
||||
const cls = ClsServiceManager.getClsService?.();
|
||||
const rid = cls?.get?.('requestId');
|
||||
const tp = cls?.get?.('traceparent');
|
||||
const ctx = context ? ` [${context}]` : '';
|
||||
const ids =
|
||||
rid || tp
|
||||
? ` rid=${rid ?? ''}${tp ? ` trace=${tp}` : ''}`
|
||||
: '';
|
||||
return `${timestamp} [${level}]${ctx} ${message}${ids}`;
|
||||
} catch {
|
||||
return `${timestamp} [${level}]${context ? ' [' + context + ']' : ''} ${message}`;
|
||||
}
|
||||
}),
|
||||
),
|
||||
}),
|
||||
new (winston.transports as any).DailyRotateFile({
|
||||
dirname: 'logs',
|
||||
filename: 'app-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
maxFiles: '14d',
|
||||
level: config.get('logLevel'),
|
||||
}),
|
||||
],
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(({ level, message, timestamp, context }) => {
|
||||
try {
|
||||
const { ClsServiceManager } = require('nestjs-cls');
|
||||
const cls = ClsServiceManager.getClsService?.();
|
||||
const rid = cls?.get?.('requestId');
|
||||
const tp = cls?.get?.('traceparent');
|
||||
const ctx = context ? ` [${context}]` : '';
|
||||
const ids =
|
||||
rid || tp
|
||||
? ` rid=${rid ?? ''}${tp ? ` trace=${tp}` : ''}`
|
||||
: '';
|
||||
return `${timestamp} [${level}]${ctx} ${message}${ids}`;
|
||||
} catch {
|
||||
return `${timestamp} [${level}]${context ? ' [' + context + ']' : ''} ${message}`;
|
||||
}
|
||||
}),
|
||||
),
|
||||
}),
|
||||
new (winston.transports as any).DailyRotateFile({
|
||||
dirname: 'logs',
|
||||
filename: 'app-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
maxFiles: '14d',
|
||||
level: config.get('logLevel'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
// 请求上下文
|
||||
@@ -208,12 +214,12 @@ const dbImports =
|
||||
JobsModule,
|
||||
// 事件总线模块
|
||||
EventBusModule,
|
||||
// 测试模块(Redis 和 Kafka 测试)
|
||||
// TestModule,
|
||||
// 配置模块(配置中心)
|
||||
ConfigModule,
|
||||
// Outbox→Kafka 转发器
|
||||
OutboxKafkaForwarderModule,
|
||||
],
|
||||
controllers: [AppController, MetricsController, HealthController],
|
||||
controllers: [AppController, MetricsController, ObHealthController],
|
||||
providers: [
|
||||
AppService,
|
||||
// 全局守卫
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Controller, Get, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import type { Response } from 'express';
|
||||
import { SwaggerConfig } from '../integrations/swaggerConfig';
|
||||
|
||||
@ApiTags('文档导航')
|
||||
@Controller()
|
||||
@@ -10,7 +9,7 @@ export class DocsNavigationController {
|
||||
@ApiOperation({ summary: 'API文档导航页面' })
|
||||
@ApiResponse({ status: 200, description: '返回API文档导航HTML页面' })
|
||||
getApiDocsNavigation(@Res() res: Response) {
|
||||
const html = SwaggerConfig.getNavigationHtml();
|
||||
const html = this.getNavigationHtml();
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
}
|
||||
@@ -134,4 +133,67 @@ export class DocsNavigationController {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getNavigationHtml(): string {
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WWJCloud API 文档导航</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji'; margin: 0; background: #f6f8fa; color: #111827; }
|
||||
.container { max-width: 960px; margin: 40px auto; padding: 0 16px; }
|
||||
.header { margin-bottom: 20px; }
|
||||
.title { font-size: 28px; color: #111827; margin: 0; }
|
||||
.desc { color: #6b7280; margin-top: 8px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; margin-top: 24px; }
|
||||
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; text-decoration: none; color: inherit; transition: box-shadow .2s, transform .2s; }
|
||||
.card:hover { box-shadow: 0 6px 20px rgba(0,0,0,.08); transform: translateY(-2px); }
|
||||
.card .icon { font-size: 22px; }
|
||||
.card .title { font-size: 18px; margin: 8px 0; color: #111827; }
|
||||
.card .text { color: #6b7280; font-size: 14px; }
|
||||
.footer { margin-top: 28px; color: #6b7280; font-size: 14px; }
|
||||
.links a { color: #2563eb; text-decoration: none; margin-right: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 class="title">WWJCloud API 文档导航</h1>
|
||||
<p class="desc">企业级后端 API 文档导航中心</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<a class="card" href="/docs">
|
||||
<div class="icon">📖</div>
|
||||
<div class="title">完整API文档</div>
|
||||
<div class="text">包含所有接口的完整API文档</div>
|
||||
</a>
|
||||
<a class="card" href="/docs/admin">
|
||||
<div class="icon">🔐</div>
|
||||
<div class="title">管理端API</div>
|
||||
<div class="text">管理后台专用接口文档</div>
|
||||
</a>
|
||||
<a class="card" href="/docs/frontend">
|
||||
<div class="icon">🌐</div>
|
||||
<div class="title">前端API</div>
|
||||
<div class="text">前端应用接口文档</div>
|
||||
</a>
|
||||
<a class="card" href="/docs/settings">
|
||||
<div class="icon">⚙️</div>
|
||||
<div class="title">系统设置API</div>
|
||||
<div class="text">系统配置和设置相关接口</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div>提示:点击上方卡片访问对应的API文档</div>
|
||||
<div class="links" style="margin-top:8px;">
|
||||
<a href="/docs-json">JSON格式文档</a>
|
||||
<a href="/health">系统健康检查</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,12 @@ export interface AppConfig {
|
||||
limit: number;
|
||||
};
|
||||
|
||||
// 启动健康检查配置
|
||||
health: {
|
||||
startupCheckEnabled: boolean;
|
||||
startupTimeoutMs: number;
|
||||
};
|
||||
|
||||
// 第三方服务配置
|
||||
thirdParty: {
|
||||
storage: {
|
||||
@@ -106,8 +112,8 @@ const defaultConfig: AppConfig = {
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: '123456',
|
||||
username: 'wwjcloud',
|
||||
password: 'wwjcloud',
|
||||
database: 'wwjcloud',
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
@@ -149,6 +155,10 @@ const defaultConfig: AppConfig = {
|
||||
ttl: 60,
|
||||
limit: 100,
|
||||
},
|
||||
health: {
|
||||
startupCheckEnabled: true,
|
||||
startupTimeoutMs: 5000,
|
||||
},
|
||||
thirdParty: {
|
||||
storage: {
|
||||
provider: 'local',
|
||||
@@ -223,6 +233,13 @@ function loadFromEnv(): Partial<AppConfig> {
|
||||
ttl: parseInt(process.env.THROTTLE_TTL || String(defaultConfig.throttle.ttl), 10),
|
||||
limit: parseInt(process.env.THROTTLE_LIMIT || String(defaultConfig.throttle.limit), 10),
|
||||
},
|
||||
health: {
|
||||
startupCheckEnabled: (process.env.STARTUP_HEALTH_CHECK || 'true').toLowerCase() !== 'false',
|
||||
startupTimeoutMs: parseInt(
|
||||
process.env.STARTUP_HEALTH_TIMEOUT_MS || String(defaultConfig.health.startupTimeoutMs),
|
||||
10,
|
||||
),
|
||||
},
|
||||
thirdParty: {
|
||||
storage: {
|
||||
provider: process.env.STORAGE_PROVIDER || defaultConfig.thirdParty.storage.provider,
|
||||
@@ -256,6 +273,7 @@ function mergeConfig(defaultConfig: AppConfig, envConfig: Partial<AppConfig>): A
|
||||
logging: { ...defaultConfig.logging, ...envConfig.logging },
|
||||
upload: { ...defaultConfig.upload, ...envConfig.upload },
|
||||
throttle: { ...defaultConfig.throttle, ...envConfig.throttle },
|
||||
health: { ...defaultConfig.health, ...envConfig.health },
|
||||
thirdParty: {
|
||||
storage: { ...defaultConfig.thirdParty.storage, ...envConfig.thirdParty?.storage },
|
||||
payment: { ...defaultConfig.thirdParty.payment, ...envConfig.thirdParty?.payment },
|
||||
@@ -323,6 +341,11 @@ export const config = {
|
||||
return appConfig.throttle;
|
||||
},
|
||||
|
||||
// 获取健康检查配置
|
||||
getHealth() {
|
||||
return appConfig.health;
|
||||
},
|
||||
|
||||
// 获取第三方服务配置
|
||||
getThirdParty() {
|
||||
return appConfig.thirdParty;
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ConfigValidationService } from '../services/configValidationService';
|
||||
import { DynamicConfigService } from '../services/dynamicConfigService';
|
||||
import { DocsNavigationController } from '../controllers/docsNavigationController';
|
||||
import { appConfig } from './appConfig';
|
||||
import { SwaggerController } from '../modules/swagger/swaggerController';
|
||||
import { SwaggerService } from '../modules/swagger/swaggerService';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -15,11 +17,12 @@ import { appConfig } from './appConfig';
|
||||
envFilePath: ['.env.local', '.env.development', '.env.production', '.env'],
|
||||
}),
|
||||
],
|
||||
controllers: [ConfigController, DocsNavigationController],
|
||||
controllers: [ConfigController, DocsNavigationController, SwaggerController],
|
||||
providers: [
|
||||
ConfigCenterService,
|
||||
ConfigValidationService,
|
||||
DynamicConfigService,
|
||||
SwaggerService,
|
||||
{
|
||||
provide: 'APP_CONFIG',
|
||||
useValue: appConfig,
|
||||
|
||||
@@ -11,9 +11,6 @@ export * from './services';
|
||||
// 配置控制器
|
||||
export * from './controllers';
|
||||
|
||||
// 集成配置
|
||||
export * from './integrations';
|
||||
|
||||
// 模块配置
|
||||
export * from './modules';
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// 集成配置导出
|
||||
export { SwaggerConfig } from './swaggerConfig';
|
||||
@@ -1,212 +0,0 @@
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Swagger API文档配置
|
||||
* 支持按前缀分组展示不同类型的API
|
||||
*/
|
||||
export class SwaggerConfig {
|
||||
/**
|
||||
* 设置API文档
|
||||
* @param app NestJS应用实例
|
||||
*/
|
||||
static setup(app: INestApplication) {
|
||||
// 统一API文档(包含所有接口)
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('WWJCloud API 文档')
|
||||
.setDescription('WWJCloud 基于 NestJS 的企业级后端 API 文档')
|
||||
.setVersion('1.0.0')
|
||||
.addBearerAuth({
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
name: 'Authorization',
|
||||
description: 'JWT Token',
|
||||
in: 'header',
|
||||
})
|
||||
.addTag('健康检查', '系统健康状态检查接口')
|
||||
.addTag('认证授权', '用户认证和授权相关接口')
|
||||
.addTag('管理端API', '管理后台专用接口')
|
||||
.addTag('前端API', '前端应用接口')
|
||||
.addTag('系统设置', '系统配置和设置接口')
|
||||
.addTag('文件上传', '文件上传和管理接口')
|
||||
.addTag('数据库管理', '数据库管理和监控接口')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('docs', app, document, {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
tagsSorter: 'alpha',
|
||||
operationsSorter: 'alpha',
|
||||
docExpansion: 'none',
|
||||
filter: true,
|
||||
showRequestDuration: true,
|
||||
},
|
||||
customSiteTitle: 'WWJCloud API 文档',
|
||||
customfavIcon: '/favicon.ico',
|
||||
customCss: `
|
||||
.swagger-ui .topbar { display: none; }
|
||||
.swagger-ui .info .title { color: #3b82f6; }
|
||||
.swagger-ui .scheme-container { background: #f8fafc; padding: 10px; border-radius: 4px; }
|
||||
`,
|
||||
});
|
||||
|
||||
console.log('📚 API文档已启动:');
|
||||
console.log(' - 完整文档: http://localhost:3001/docs');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API文档导航页面HTML
|
||||
* @returns HTML字符串
|
||||
*/
|
||||
static getNavigationHtml(): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WWJCloud API 文档导航</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
padding: 40px;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.header h1 {
|
||||
color: #2d3748;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.header p {
|
||||
color: #718096;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.docs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.doc-card {
|
||||
background: #f7fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
display: block;
|
||||
}
|
||||
.doc-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||||
border-color: #4299e1;
|
||||
}
|
||||
.doc-card.complete { border-color: #3b82f6; background: #eff6ff; }
|
||||
.doc-card.admin { border-color: #dc2626; background: #fef2f2; }
|
||||
.doc-card.frontend { border-color: #059669; background: #f0fdf4; }
|
||||
.doc-card.settings { border-color: #7c3aed; background: #faf5ff; }
|
||||
.doc-card h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 8px;
|
||||
color: #2d3748;
|
||||
}
|
||||
.doc-card.complete h3 { color: #3b82f6; }
|
||||
.doc-card.admin h3 { color: #dc2626; }
|
||||
.doc-card.frontend h3 { color: #059669; }
|
||||
.doc-card.settings h3 { color: #7c3aed; }
|
||||
.doc-card p {
|
||||
color: #4a5568;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.doc-card .badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.doc-card.complete .badge { background: #dbeafe; color: #1e40af; }
|
||||
.doc-card.admin .badge { background: #fee2e2; color: #991b1b; }
|
||||
.doc-card.frontend .badge { background: #dcfce7; color: #166534; }
|
||||
.doc-card.settings .badge { background: #f3e8ff; color: #6b21a8; }
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
color: #718096;
|
||||
}
|
||||
.footer a {
|
||||
color: #4299e1;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 WWJCloud API</h1>
|
||||
<p>企业级后端API文档导航中心</p>
|
||||
</div>
|
||||
|
||||
<div class="docs-grid">
|
||||
<a href="/docs" class="doc-card complete">
|
||||
<h3>📖 完整API文档</h3>
|
||||
<p>包含所有接口的完整API文档,适合开发者全面了解系统功能</p>
|
||||
<span class="badge">Complete</span>
|
||||
</a>
|
||||
|
||||
<a href="/docs/admin" class="doc-card admin">
|
||||
<h3>🔐 管理端API</h3>
|
||||
<p>管理后台专用接口文档,包含管理员、权限、系统配置等功能</p>
|
||||
<span class="badge">Admin</span>
|
||||
</a>
|
||||
|
||||
<a href="/docs/frontend" class="doc-card frontend">
|
||||
<h3>🌐 前端API</h3>
|
||||
<p>前端应用接口文档,包含用户认证、会员功能等前端专用接口</p>
|
||||
<span class="badge">Frontend</span>
|
||||
</a>
|
||||
|
||||
<a href="/docs/settings" class="doc-card settings">
|
||||
<h3>⚙️ 系统设置API</h3>
|
||||
<p>系统配置和设置相关接口,包含存储、支付、短信等配置管理</p>
|
||||
<span class="badge">Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>💡 提示:点击上方卡片访问对应的API文档 |
|
||||
<a href="/docs-json" target="_blank">JSON格式文档</a> |
|
||||
<a href="/health" target="_blank">系统健康检查</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
53
wwjcloud/src/config/modules/swagger/swaggerController.ts
Normal file
53
wwjcloud/src/config/modules/swagger/swaggerController.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Controller, Get, UnauthorizedException } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import type { Request } from 'express';
|
||||
import { SwaggerService } from './swaggerService';
|
||||
import { ConfigCenterService } from '../../services/configCenterService';
|
||||
import { Req } from '@nestjs/common';
|
||||
|
||||
@ApiTags('文档')
|
||||
@Controller()
|
||||
export class SwaggerController {
|
||||
constructor(
|
||||
private readonly docs: SwaggerService,
|
||||
private readonly configCenter: ConfigCenterService,
|
||||
) {}
|
||||
|
||||
private verifyToken(req: Request) {
|
||||
const requiredToken = this.configCenter.get<string>('swagger.token', '');
|
||||
if (!requiredToken) {
|
||||
throw new UnauthorizedException('Swagger token not configured');
|
||||
}
|
||||
const auth = req.headers['authorization'] || '';
|
||||
const token = typeof auth === 'string' && auth.startsWith('Bearer ')
|
||||
? auth.slice('Bearer '.length).trim()
|
||||
: '';
|
||||
if (token !== requiredToken) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
@Get('api-json')
|
||||
@ApiOperation({ summary: '获取完整 API 文档 JSON' })
|
||||
@ApiResponse({ status: 200, description: '返回完整 Swagger 文档 JSON' })
|
||||
getDocsJson(@Req() req: Request) {
|
||||
this.verifyToken(req);
|
||||
return this.docs.getDocument();
|
||||
}
|
||||
|
||||
@Get('api/admin-json')
|
||||
@ApiOperation({ summary: '获取管理端 API 文档 JSON' })
|
||||
@ApiResponse({ status: 200, description: '返回管理端 Swagger 文档 JSON' })
|
||||
getAdminDocsJson(@Req() req: Request) {
|
||||
this.verifyToken(req);
|
||||
return this.docs.getAdminDocument();
|
||||
}
|
||||
|
||||
@Get('api/frontend-json')
|
||||
@ApiOperation({ summary: '获取前端 API 文档 JSON' })
|
||||
@ApiResponse({ status: 200, description: '返回前端 Swagger 文档 JSON' })
|
||||
getFrontendDocsJson(@Req() req: Request) {
|
||||
this.verifyToken(req);
|
||||
return this.docs.getFrontendDocument();
|
||||
}
|
||||
}
|
||||
70
wwjcloud/src/config/modules/swagger/swaggerService.ts
Normal file
70
wwjcloud/src/config/modules/swagger/swaggerService.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import type { OpenAPIObject } from '@nestjs/swagger';
|
||||
import { ConfigCenterService } from '../../services/configCenterService';
|
||||
|
||||
@Injectable()
|
||||
export class SwaggerService {
|
||||
private initialized = false;
|
||||
private document: OpenAPIObject | null = null;
|
||||
private adminDocument: OpenAPIObject | null = null;
|
||||
private frontendDocument: OpenAPIObject | null = null;
|
||||
|
||||
constructor(private readonly configCenter: ConfigCenterService) {}
|
||||
|
||||
getDocument() {
|
||||
return this.document;
|
||||
}
|
||||
|
||||
getAdminDocument() {
|
||||
return this.adminDocument;
|
||||
}
|
||||
|
||||
getFrontendDocument() {
|
||||
return this.frontendDocument;
|
||||
}
|
||||
|
||||
setup(app: INestApplication) {
|
||||
if (this.initialized) return;
|
||||
|
||||
const enabled = this.configCenter.get<boolean>('swagger.enabled', true);
|
||||
if (!enabled) return;
|
||||
|
||||
const title = this.configCenter.get<string>('swagger.title', 'WWJCloud API 文档');
|
||||
const description = this.configCenter.get<string>('swagger.description', 'WWJCloud 基于 NestJS 的企业级后端 API 文档');
|
||||
const version = this.configCenter.get<string>('swagger.version', '1.0.0');
|
||||
const enableAuth = this.configCenter.get<boolean>('swagger.auth.enabled', true);
|
||||
|
||||
const builder = new DocumentBuilder().setTitle(title).setDescription(description).setVersion(version);
|
||||
if (enableAuth) {
|
||||
builder.addBearerAuth({
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
name: 'Authorization',
|
||||
description: 'JWT Token',
|
||||
in: 'header',
|
||||
});
|
||||
}
|
||||
|
||||
const fullDoc = SwaggerModule.createDocument(app, builder.build());
|
||||
this.document = fullDoc;
|
||||
|
||||
const splitByPrefix = (
|
||||
doc: OpenAPIObject,
|
||||
prefix: string,
|
||||
): OpenAPIObject => {
|
||||
const paths: Record<string, any> = {};
|
||||
Object.keys(doc.paths || {}).forEach((p) => {
|
||||
if (p.startsWith(prefix)) paths[p] = (doc.paths as any)[p];
|
||||
});
|
||||
return { ...doc, paths } as OpenAPIObject;
|
||||
};
|
||||
|
||||
this.adminDocument = splitByPrefix(fullDoc, '/adminapi');
|
||||
this.frontendDocument = splitByPrefix(fullDoc, '/api');
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
12
wwjcloud/src/core/event/outboxKafkaForwarder.module.ts
Normal file
12
wwjcloud/src/core/event/outboxKafkaForwarder.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EventEntity } from '@wwjCore/queue/entities/event.entity';
|
||||
import { OutboxKafkaForwarderService } from './outboxKafkaForwarder.service';
|
||||
import { VendorModule } from '@wwjVendor/index';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([EventEntity]), VendorModule],
|
||||
providers: [OutboxKafkaForwarderService],
|
||||
exports: [OutboxKafkaForwarderService],
|
||||
})
|
||||
export class OutboxKafkaForwarderModule {}
|
||||
65
wwjcloud/src/core/event/outboxKafkaForwarder.service.ts
Normal file
65
wwjcloud/src/core/event/outboxKafkaForwarder.service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LessThanOrEqual, Repository } from 'typeorm';
|
||||
import { EventEntity } from '@wwjCore/queue/entities/event.entity';
|
||||
import { KafkaProvider } from '@wwjVendor/event/kafka.provider';
|
||||
|
||||
@Injectable()
|
||||
export class OutboxKafkaForwarderService {
|
||||
private readonly logger = new Logger(OutboxKafkaForwarderService.name);
|
||||
private readonly batchSize = 50;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(EventEntity)
|
||||
private readonly eventRepo: Repository<EventEntity>,
|
||||
private readonly kafka: KafkaProvider,
|
||||
) {}
|
||||
|
||||
@Cron(CronExpression.EVERY_SECOND)
|
||||
async forward() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const events = await this.eventRepo.find({
|
||||
where: {
|
||||
status: 'pending',
|
||||
next_retry_at: LessThanOrEqual(now),
|
||||
},
|
||||
order: { occurred_at: 'ASC' },
|
||||
take: this.batchSize,
|
||||
});
|
||||
|
||||
for (const e of events) {
|
||||
try {
|
||||
const topic = e.event_type; // 建议规范如 iam.user.v1
|
||||
const key = e.aggregate_id || e.event_id;
|
||||
const message = {
|
||||
event_id: e.event_id,
|
||||
event_type: e.event_type,
|
||||
aggregate_id: e.aggregate_id,
|
||||
aggregate_type: e.aggregate_type,
|
||||
site_id: e.site_id,
|
||||
trace_id: e.trace_id,
|
||||
event_version: e.event_version,
|
||||
occurred_at: e.occurred_at,
|
||||
data: JSON.parse(e.event_data),
|
||||
};
|
||||
await this.kafka.publish(topic, key, message);
|
||||
await this.eventRepo.update(e.id, {
|
||||
status: 'processed',
|
||||
processed_at: Math.floor(Date.now() / 1000),
|
||||
last_error: null,
|
||||
});
|
||||
} catch (err: any) {
|
||||
const retry = e.retry_count + 1;
|
||||
const delay = Math.min(60 * 60, Math.pow(2, retry) * 10); // 最大回退1小时
|
||||
await this.eventRepo.update(e.id, {
|
||||
status: 'pending',
|
||||
retry_count: retry,
|
||||
last_error: String(err?.message ?? err),
|
||||
next_retry_at: Math.floor(Date.now() / 1000) + delay,
|
||||
});
|
||||
this.logger.error(`Forward event failed: id=${e.id} type=${e.event_type} ${err?.message ?? err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './healthController';
|
||||
import { HealthzController } from './healthzController';
|
||||
import { HealthService } from './healthService';
|
||||
import { QueueModule } from '@wwjCore/queue/queueModule';
|
||||
@@ -15,7 +14,6 @@ import { EventModule } from '@wwjCore/event/eventModule';
|
||||
EventModule,
|
||||
],
|
||||
controllers: [
|
||||
HealthController,
|
||||
HealthzController,
|
||||
],
|
||||
providers: [HealthService],
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class DbHealthIndicator {
|
||||
readonly name = 'db';
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async check(): Promise<boolean> {
|
||||
try {
|
||||
// TODO: 实现真实的数据库健康检查
|
||||
// 暂时返回 true,表示服务可用
|
||||
const withTimeout = async <T>(p: Promise<T>, ms: number) =>
|
||||
await Promise.race<T>([
|
||||
p,
|
||||
new Promise<T>((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)),
|
||||
]);
|
||||
await withTimeout(this.dataSource.query('SELECT 1'), 3000);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { KafkaProvider } from '@wwjVendor/event/kafka.provider';
|
||||
|
||||
@Injectable()
|
||||
export class EventBusHealthIndicator {
|
||||
readonly name = 'eventbus';
|
||||
constructor(private readonly kafkaProvider: KafkaProvider) {}
|
||||
|
||||
async check(): Promise<boolean> {
|
||||
try {
|
||||
// TODO: 实现真实的事件总线健康检查
|
||||
// 暂时返回 true,表示服务可用
|
||||
const withTimeout = async <T>(p: Promise<T>, ms: number) =>
|
||||
await Promise.race<T>([
|
||||
p,
|
||||
new Promise<T>((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)),
|
||||
]);
|
||||
await withTimeout(this.kafkaProvider.ensure(), 3000);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BullQueueProvider } from '@wwjVendor/queue/bullmq.provider';
|
||||
|
||||
@Injectable()
|
||||
export class QueueHealthIndicator {
|
||||
readonly name = 'queue';
|
||||
constructor(private readonly bullQueueProvider: BullQueueProvider) {}
|
||||
|
||||
async check(): Promise<boolean> {
|
||||
try {
|
||||
// TODO: 实现真实的队列健康检查
|
||||
// 暂时返回 true,表示服务可用
|
||||
return true;
|
||||
const withTimeout = async <T>(p: Promise<T>, ms: number) =>
|
||||
await Promise.race<T>([
|
||||
p,
|
||||
new Promise<T>((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)),
|
||||
]);
|
||||
const ok = await withTimeout(this.bullQueueProvider.healthCheck(), 3000);
|
||||
return !!ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
// import { RedisProvider } from '../../../vendor/redis/redis.provider';
|
||||
import { RedisProvider } from '@wwjVendor/redis/redis.provider';
|
||||
|
||||
@Injectable()
|
||||
export class RedisHealthIndicator {
|
||||
readonly name = 'redis';
|
||||
constructor() {}
|
||||
constructor(private readonly redisProvider: RedisProvider) {}
|
||||
|
||||
check(): Promise<boolean> {
|
||||
async check(): Promise<boolean> {
|
||||
try {
|
||||
// TODO: 实现 Redis 健康检查
|
||||
// const pong = await this.redisProvider.ping();
|
||||
// return pong === 'PONG' || pong === 'pong';
|
||||
return Promise.resolve(true);
|
||||
const withTimeout = async <T>(p: Promise<T>, ms: number) =>
|
||||
await Promise.race<T>([
|
||||
p,
|
||||
new Promise<T>((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)),
|
||||
]);
|
||||
const pong = await withTimeout(this.redisProvider.ping(), 2000);
|
||||
return pong?.toString().toUpperCase() === 'PONG';
|
||||
} catch {
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@Injectable()
|
||||
export class StorageHealthIndicator {
|
||||
readonly name = 'storage';
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
check(): Promise<boolean> {
|
||||
async check(): Promise<boolean> {
|
||||
try {
|
||||
// TODO: 实现真实的存储健康检查
|
||||
// 暂时返回 true,表示服务可用
|
||||
return Promise.resolve(true);
|
||||
const provider = this.configService.get<string>('thirdParty.storage.provider') || 'local';
|
||||
if (provider === 'local') {
|
||||
const uploadPath = this.configService.get<string>('upload.path') || 'uploads/';
|
||||
const testDir = path.resolve(process.cwd(), uploadPath);
|
||||
const testFile = path.join(testDir, `.healthcheck_${Date.now()}.tmp`);
|
||||
try {
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
await fs.writeFile(testFile, 'ok');
|
||||
await fs.unlink(testFile);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// TODO: 其他存储适配器的轻量健康检查(如 headBucket)
|
||||
return true;
|
||||
} catch {
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,8 @@ export class DatabaseQueueProvider implements ITaskQueueProvider, IEventBusProvi
|
||||
eventRecord.event_type = event.eventType;
|
||||
eventRecord.aggregate_id = event.aggregateId;
|
||||
eventRecord.aggregate_type = event.fromDomain || 'default'; // 使用源域或默认值
|
||||
eventRecord.site_id = (event as any).tenantId ? Number((event as any).tenantId) : 0;
|
||||
eventRecord.trace_id = (event as any).traceId ?? null;
|
||||
eventRecord.event_data = JSON.stringify(event.data);
|
||||
eventRecord.event_version = parseInt(event.version) || 1;
|
||||
eventRecord.occurred_at = Math.floor(new Date(event.occurredAt).getTime() / 1000); // 转换字符串为时间戳
|
||||
@@ -230,12 +232,13 @@ export class DatabaseQueueProvider implements ITaskQueueProvider, IEventBusProvi
|
||||
for (const eventRecord of failedEvents) {
|
||||
try {
|
||||
// 重构事件对象
|
||||
const parsedHeaders = eventRecord.headers ? JSON.parse(eventRecord.headers) : {};
|
||||
const event: DomainEvent = {
|
||||
eventType: eventRecord.event_type,
|
||||
aggregateId: eventRecord.aggregate_id,
|
||||
tenantId: '', // 从 event_data 中解析
|
||||
tenantId: (eventRecord as any).site_id ? String((eventRecord as any).site_id) : '',
|
||||
idempotencyKey: eventRecord.event_id,
|
||||
traceId: '', // 从 headers 中解析
|
||||
traceId: (eventRecord as any).trace_id || parsedHeaders.traceId || parsedHeaders['traceparent'] || '',
|
||||
data: JSON.parse(eventRecord.event_data),
|
||||
occurredAt: new Date(eventRecord.occurred_at * 1000).toISOString(),
|
||||
version: eventRecord.event_version.toString(),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
@Index(['event_type', 'processed_at'])
|
||||
@Index(['aggregate_id', 'aggregate_type'])
|
||||
@Index(['occurred_at'])
|
||||
@Index(['site_id', 'status'])
|
||||
export class EventEntity extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@@ -37,6 +38,12 @@ export class EventEntity extends BaseEntity {
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
aggregate_type: string;
|
||||
|
||||
/**
|
||||
* 链路追踪ID
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 128, nullable: true })
|
||||
trace_id: string | null;
|
||||
|
||||
/**
|
||||
* 事件数据(JSON格式)
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TaskQueueAdapterType, EventBusAdapterType } from './queueTypes';
|
||||
|
||||
export interface QueueConfig {
|
||||
@@ -28,131 +29,102 @@ export interface QueueConfig {
|
||||
export class QueueFactoryService {
|
||||
private readonly logger = new Logger(QueueFactoryService.name);
|
||||
|
||||
// 使用固定配置,避免硬编码
|
||||
private readonly defaultConfig: QueueConfig = {
|
||||
taskAdapter: TaskQueueAdapterType.DATABASE_OUTBOX,
|
||||
eventAdapter: EventBusAdapterType.DATABASE_OUTBOX,
|
||||
redis: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
password: '',
|
||||
db: 0,
|
||||
},
|
||||
queue: {
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: 50,
|
||||
defaultAttempts: 3,
|
||||
backoffDelay: 2000,
|
||||
},
|
||||
kafka: {
|
||||
clientId: 'wwjcloud-backend',
|
||||
brokers: ['localhost:9092'],
|
||||
groupId: 'wwjcloud-group',
|
||||
topicPrefix: 'domain-events',
|
||||
},
|
||||
};
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
// 添加适配器属性
|
||||
private readonly taskQueueAdapter = 'database-outbox';
|
||||
private readonly eventBusAdapter = 'database-outbox';
|
||||
private getConfig(): QueueConfig {
|
||||
return {
|
||||
taskAdapter: (this.config.get<string>('queue.taskAdapter') as TaskQueueAdapterType) || TaskQueueAdapterType.DATABASE_OUTBOX,
|
||||
eventAdapter: (this.config.get<string>('queue.eventAdapter') as EventBusAdapterType) || EventBusAdapterType.DATABASE_OUTBOX,
|
||||
redis: {
|
||||
host: this.config.get<string>('redis.host'),
|
||||
port: Number(this.config.get<number>('redis.port')),
|
||||
password: this.config.get<string>('redis.password') || '',
|
||||
db: Number(this.config.get<number>('redis.db') || 0),
|
||||
},
|
||||
queue: {
|
||||
removeOnComplete: Number(this.config.get<number>('queue.removeOnComplete') || 100),
|
||||
removeOnFail: Number(this.config.get<number>('queue.removeOnFail') || 50),
|
||||
defaultAttempts: Number(this.config.get<number>('queue.defaultAttempts') || 3),
|
||||
backoffDelay: Number(this.config.get<number>('queue.backoffDelay') || 2000),
|
||||
},
|
||||
kafka: {
|
||||
clientId: this.config.get<string>('kafka.clientId'),
|
||||
brokers: this.config.get<string[]>('kafka.brokers'),
|
||||
groupId: this.config.get<string>('kafka.groupId') || 'wwjcloud-group',
|
||||
topicPrefix: this.config.get<string>('kafka.topicPrefix') || 'domain-events',
|
||||
},
|
||||
} as QueueConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务队列配置
|
||||
*/
|
||||
getTaskQueueConfig(): Partial<QueueConfig> {
|
||||
const cfg = this.getConfig();
|
||||
return {
|
||||
taskAdapter: this.defaultConfig.taskAdapter,
|
||||
redis: this.defaultConfig.redis,
|
||||
queue: this.defaultConfig.queue,
|
||||
taskAdapter: cfg.taskAdapter,
|
||||
redis: cfg.redis,
|
||||
queue: cfg.queue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件总线配置
|
||||
*/
|
||||
getEventBusConfig(): Partial<QueueConfig> {
|
||||
const cfg = this.getConfig();
|
||||
return {
|
||||
eventAdapter: this.defaultConfig.eventAdapter,
|
||||
redis: this.defaultConfig.redis,
|
||||
kafka: this.defaultConfig.kafka,
|
||||
eventAdapter: cfg.eventAdapter,
|
||||
redis: cfg.redis,
|
||||
kafka: cfg.kafka,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整配置
|
||||
*/
|
||||
getFullConfig(): QueueConfig {
|
||||
return { ...this.defaultConfig };
|
||||
return this.getConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Redis 配置
|
||||
*/
|
||||
getRedisConfig() {
|
||||
return this.defaultConfig.redis;
|
||||
return this.getConfig().redis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列配置
|
||||
*/
|
||||
getQueueConfig() {
|
||||
return this.defaultConfig.queue;
|
||||
return this.getConfig().queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Kafka 配置
|
||||
*/
|
||||
getKafkaConfig() {
|
||||
return this.defaultConfig.kafka;
|
||||
return this.getConfig().kafka;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务队列适配器类型
|
||||
*/
|
||||
getTaskAdapterType(): TaskQueueAdapterType {
|
||||
return this.defaultConfig.taskAdapter;
|
||||
return this.getConfig().taskAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件总线适配器类型
|
||||
*/
|
||||
getEventAdapterType(): EventBusAdapterType {
|
||||
return this.defaultConfig.eventAdapter;
|
||||
return this.getConfig().eventAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
*/
|
||||
validateConfig(config: Partial<QueueConfig>): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 验证 Redis 配置
|
||||
if (config.redis) {
|
||||
if (!config.redis.host) {
|
||||
errors.push('Redis host is required');
|
||||
}
|
||||
if (config.redis.port < 1 || config.redis.port > 65535) {
|
||||
if (config.redis.port === undefined || config.redis.port < 1 || config.redis.port > 65535) {
|
||||
errors.push('Redis port must be between 1 and 65535');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证队列配置
|
||||
if (config.queue) {
|
||||
if (config.queue.removeOnComplete < 0) {
|
||||
if ((config.queue.removeOnComplete ?? 0) < 0) {
|
||||
errors.push('removeOnComplete must be non-negative');
|
||||
}
|
||||
if (config.queue.removeOnFail < 0) {
|
||||
if ((config.queue.removeOnFail ?? 0) < 0) {
|
||||
errors.push('removeOnFail must be non-negative');
|
||||
}
|
||||
if (config.queue.defaultAttempts < 1) {
|
||||
if ((config.queue.defaultAttempts ?? 0) < 1) {
|
||||
errors.push('defaultAttempts must be at least 1');
|
||||
}
|
||||
if (config.queue.backoffDelay < 0) {
|
||||
if ((config.queue.backoffDelay ?? 0) < 0) {
|
||||
errors.push('backoffDelay must be non-negative');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 Kafka 配置
|
||||
if (config.kafka) {
|
||||
if (!config.kafka.clientId) {
|
||||
errors.push('Kafka clientId is required');
|
||||
@@ -171,33 +143,27 @@ export class QueueFactoryService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置摘要
|
||||
*/
|
||||
getConfigSummary(): Record<string, any> {
|
||||
const cfg = this.getConfig();
|
||||
return {
|
||||
taskAdapter: this.defaultConfig.taskAdapter,
|
||||
eventAdapter: this.defaultConfig.eventAdapter,
|
||||
taskAdapter: cfg.taskAdapter,
|
||||
eventAdapter: cfg.eventAdapter,
|
||||
redis: {
|
||||
host: this.defaultConfig.redis.host,
|
||||
port: this.defaultConfig.redis.port,
|
||||
db: this.defaultConfig.redis.db,
|
||||
},
|
||||
queue: this.defaultConfig.queue,
|
||||
kafka: {
|
||||
clientId: this.defaultConfig.kafka.clientId,
|
||||
brokers: this.defaultConfig.kafka.brokers,
|
||||
groupId: this.defaultConfig.kafka.groupId,
|
||||
topicPrefix: this.defaultConfig.kafka.topicPrefix,
|
||||
host: cfg.redis.host,
|
||||
port: cfg.redis.port,
|
||||
db: cfg.redis.db,
|
||||
},
|
||||
queue: cfg.queue,
|
||||
kafka: cfg.kafka,
|
||||
};
|
||||
}
|
||||
|
||||
getTaskQueueProvider() {
|
||||
return this.taskQueueAdapter;
|
||||
// 对外暴露适配器标识可供装配
|
||||
return this.getConfig().taskAdapter;
|
||||
}
|
||||
|
||||
getEventBusProvider() {
|
||||
return this.eventBusAdapter;
|
||||
return this.getConfig().eventAdapter;
|
||||
}
|
||||
}
|
||||
@@ -1,203 +1,35 @@
|
||||
import { Injectable, OnModuleDestroy, Inject } from '@nestjs/common';
|
||||
import { ITaskQueueProvider, ITaskQueue, TaskJobOptions, TaskProcessor, TaskJob } from '@wwjCore/interfaces/queue.interface';
|
||||
import type { Queue, QueueOptions } from 'bull';
|
||||
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||
import { ITaskQueueProvider, TaskJobOptions, TaskProcessor, TaskJob, ITaskQueue } from '@wwjCore/interfaces/queue.interface';
|
||||
|
||||
/**
|
||||
* 基于Redis的任务队列提供者实现
|
||||
* 使用Bull/BullMQ实现高性能任务队列
|
||||
* Deprecated: 该实现基于 bull,已统一迁移到 BullMQ(见 vendor/queue/bullmq.provider.ts)。
|
||||
* 保留文件以兼容旧引用,但不再在工厂或装配中被选择。
|
||||
*/
|
||||
@Injectable()
|
||||
export class RedisTaskQueueProvider implements ITaskQueueProvider, OnModuleDestroy {
|
||||
private readonly queues = new Map<string, RedisTaskQueue>();
|
||||
|
||||
constructor(
|
||||
@Inject('REDIS_QUEUE_OPTIONS')
|
||||
private readonly options: Partial<QueueOptions> = {},
|
||||
) {}
|
||||
|
||||
getQueue(name: string): ITaskQueue {
|
||||
let queue = this.queues.get(name);
|
||||
if (!queue) {
|
||||
queue = new RedisTaskQueue(name, this.options);
|
||||
this.queues.set(name, queue);
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
||||
async addJob<T = any>(queueName: string, jobName: string, data: T, options?: TaskJobOptions): Promise<void> {
|
||||
const queue = this.getQueue(queueName);
|
||||
await queue.addJob(jobName, data, options);
|
||||
}
|
||||
|
||||
async process<T = any>(queueName: string, processor: TaskProcessor<T>): Promise<void> {
|
||||
const queue = this.getQueue(queueName);
|
||||
await queue.process(processor);
|
||||
}
|
||||
|
||||
async getQueueStatus(queueName: string): Promise<any> {
|
||||
const queue = this.getQueue(queueName);
|
||||
return queue.getStats();
|
||||
}
|
||||
|
||||
async pause(queueName: string): Promise<void> {
|
||||
const queue = this.getQueue(queueName);
|
||||
await queue.pause();
|
||||
}
|
||||
|
||||
async resume(queueName: string): Promise<void> {
|
||||
const queue = this.getQueue(queueName);
|
||||
await queue.resume();
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
// 检查所有队列的健康状态
|
||||
for (const queue of this.queues.values()) {
|
||||
const stats = await queue.getStats();
|
||||
if (!stats) return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
for (const queue of this.queues.values()) {
|
||||
await queue.close();
|
||||
}
|
||||
this.queues.clear();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis任务队列实现
|
||||
*/
|
||||
class RedisTaskQueue implements ITaskQueue {
|
||||
private readonly queue: Queue;
|
||||
private readonly processors = new Map<string, TaskProcessor>();
|
||||
|
||||
constructor(name: string, options: Partial<QueueOptions>) {
|
||||
const Bull = require('bull');
|
||||
this.queue = new Bull(name, {
|
||||
...options,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: 50,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 2000,
|
||||
},
|
||||
...options.defaultJobOptions,
|
||||
},
|
||||
}) as Queue;
|
||||
|
||||
// 设置全局处理器
|
||||
this.queue.process('*', async (job) => {
|
||||
const processor = this.processors.get(job.name);
|
||||
if (!processor) {
|
||||
throw new Error(`No processor found for job type: ${job.name}`);
|
||||
}
|
||||
|
||||
const taskJob: TaskJob = {
|
||||
id: job.id.toString(),
|
||||
type: job.name,
|
||||
data: job.data,
|
||||
attemptsMade: job.attemptsMade,
|
||||
timestamp: job.timestamp,
|
||||
};
|
||||
|
||||
return await processor(taskJob);
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
this.queue.on('failed', (job, err) => {
|
||||
console.error(`Task ${job.name}:${job.id} failed:`, err);
|
||||
});
|
||||
|
||||
this.queue.on('completed', (job) => {
|
||||
console.log(`Task ${job.name}:${job.id} completed`);
|
||||
});
|
||||
}
|
||||
|
||||
async add(jobType: string, payload: any, options: TaskJobOptions = {}): Promise<void> {
|
||||
const jobOptions: any = {
|
||||
attempts: options.attempts ?? 3,
|
||||
backoff: options.backoff
|
||||
? { type: options.backoff.type, delay: options.backoff.delay }
|
||||
: undefined,
|
||||
removeOnComplete: options.removeOnComplete ?? true,
|
||||
removeOnFail: options.removeOnFail ?? false,
|
||||
delay: options.delay ?? 0,
|
||||
priority: options.priority,
|
||||
};
|
||||
|
||||
await this.queue.add(jobType, payload, jobOptions);
|
||||
}
|
||||
|
||||
async addJob(jobType: string, payload: any, options: TaskJobOptions = {}): Promise<void> {
|
||||
await this.add(jobType, payload, options);
|
||||
}
|
||||
|
||||
process(jobType: string, processor: TaskProcessor): void;
|
||||
process<T = any>(processor: TaskProcessor<T>): Promise<void>;
|
||||
process(jobTypeOrProcessor: string | TaskProcessor, processor?: TaskProcessor): void | Promise<void> {
|
||||
if (typeof jobTypeOrProcessor === 'string') {
|
||||
// 旧的方法签名:process(jobType: string, processor: TaskProcessor): void
|
||||
this.processors.set(jobTypeOrProcessor, processor!);
|
||||
} else {
|
||||
// 新的方法签名:process<T = any>(processor: TaskProcessor<T>): Promise<void>
|
||||
this.processors.set('*', jobTypeOrProcessor);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.queue.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列统计信息
|
||||
*/
|
||||
async getStats() {
|
||||
const waiting = await this.queue.getWaiting();
|
||||
const active = await this.queue.getActive();
|
||||
const completed = await this.queue.getCompleted();
|
||||
const failed = await this.queue.getFailed();
|
||||
const delayed = await this.queue.getDelayed();
|
||||
|
||||
getQueue(_name: string): ITaskQueue {
|
||||
return {
|
||||
waiting: waiting.length,
|
||||
active: active.length,
|
||||
completed: completed.length,
|
||||
failed: failed.length,
|
||||
delayed: delayed.length,
|
||||
};
|
||||
async add(): Promise<void> { throw new Error('Deprecated: Use BullMQ provider instead.'); },
|
||||
async addJob(): Promise<void> { throw new Error('Deprecated: Use BullMQ provider instead.'); },
|
||||
async process(): Promise<void> { throw new Error('Deprecated: Use BullMQ provider instead.'); },
|
||||
async getStats(): Promise<any> { return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 }; },
|
||||
async pause(): Promise<void> { /* no-op */ },
|
||||
async resume(): Promise<void> { /* no-op */ },
|
||||
async close(): Promise<void> { /* no-op */ },
|
||||
} as ITaskQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理队列
|
||||
*/
|
||||
async clean(grace: number = 0, status: 'completed' | 'failed' = 'completed') {
|
||||
return await this.queue.clean(grace, status);
|
||||
async addJob<T = any>(_queueName: string, _jobName: string, _data: T, _options?: TaskJobOptions): Promise<void> {
|
||||
throw new Error('Deprecated: Use BullMQ (vendor/queue/bullmq.provider.ts) instead.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停队列
|
||||
*/
|
||||
async pause() {
|
||||
return await this.queue.pause();
|
||||
async process<T = any>(_queueName: string, _processor: TaskProcessor<T>): Promise<void> {
|
||||
throw new Error('Deprecated: Use BullMQ (vendor/queue/bullmq.provider.ts) instead.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复队列
|
||||
*/
|
||||
async resume() {
|
||||
return await this.queue.resume();
|
||||
async getQueueStatus(_queueName: string): Promise<any> {
|
||||
return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
|
||||
}
|
||||
async pause(_queueName: string): Promise<void> {}
|
||||
async resume(_queueName: string): Promise<void> {}
|
||||
async healthCheck(): Promise<boolean> { return false; }
|
||||
async close(): Promise<void> {}
|
||||
async onModuleDestroy() { await this.close(); }
|
||||
}
|
||||
@@ -2,13 +2,19 @@ import 'dotenv/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { SwaggerConfig } from './config/integrations/swaggerConfig';
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
||||
import multipart from '@fastify/multipart';
|
||||
import { DbHealthIndicator } from './core/observability/health/indicators/db.indicator';
|
||||
import { RedisHealthIndicator } from './core/observability/health/indicators/redis.indicator';
|
||||
import { EventBusHealthIndicator } from './core/observability/health/indicators/eventbus.indicator';
|
||||
import { QueueHealthIndicator } from './core/observability/health/indicators/queue.indicator';
|
||||
import { StorageHealthIndicator } from './core/observability/health/indicators/storage.indicator';
|
||||
import { config } from './config/core/appConfig';
|
||||
import { SwaggerService } from './config/modules/swagger/swaggerService';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
@@ -17,13 +23,14 @@ async function bootstrap() {
|
||||
{ bufferLogs: true },
|
||||
);
|
||||
|
||||
// 注册 multipart 支持(类型兼容:cast any 规避 fastify 多版本类型冲突)
|
||||
// 注册 multipart 支持(参数化自配置中心,仅使用已存在的键)
|
||||
const uploadCfg = config.getUpload();
|
||||
await (app as any).register(multipart as any, {
|
||||
limits: {
|
||||
fieldNameSize: 100,
|
||||
fieldSize: 1024 * 1024, // 1MB
|
||||
fieldSize: 1024 * 1024,
|
||||
fields: 10,
|
||||
fileSize: 1024 * 1024 * 50, // 50MB 单文件
|
||||
fileSize: (uploadCfg as any).maxSize ?? 1024 * 1024 * 50,
|
||||
files: 10,
|
||||
headerPairs: 2000,
|
||||
},
|
||||
@@ -41,12 +48,41 @@ async function bootstrap() {
|
||||
|
||||
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
|
||||
|
||||
// 设置分组API文档
|
||||
SwaggerConfig.setup(app);
|
||||
// 设置 API 文档(配置中心)
|
||||
app.get(SwaggerService).setup(app);
|
||||
|
||||
const port =
|
||||
Number(process.env.PORT) ||
|
||||
(process.env.NODE_ENV === 'development' ? 3001 : 3000);
|
||||
await app.listen({ port, host: '0.0.0.0' });
|
||||
// 启动期 fail-fast(受配置中心控制)
|
||||
const healthCfg = config.getHealth();
|
||||
if (healthCfg.startupCheckEnabled) {
|
||||
await app.init();
|
||||
const checks: Array<() => Promise<unknown>> = [
|
||||
() => app.get(DbHealthIndicator).check(),
|
||||
() => app.get(RedisHealthIndicator).check(),
|
||||
() => app.get(EventBusHealthIndicator).check(),
|
||||
() => app.get(QueueHealthIndicator).check(),
|
||||
() => app.get(StorageHealthIndicator).check(),
|
||||
];
|
||||
await Promise.all(checks.map((fn) => fn()));
|
||||
}
|
||||
|
||||
const host = '0.0.0.0';
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
|
||||
await app.listen(port, host);
|
||||
|
||||
try {
|
||||
const url = await app.getUrl();
|
||||
const base = url.replace(/\/$/, '');
|
||||
const logger = app.get(WINSTON_MODULE_NEST_PROVIDER) as any;
|
||||
const msg = [
|
||||
`Server started: ${base}`,
|
||||
`Docs (full): ${base}/api-json`,
|
||||
`Docs (admin): ${base}/api/admin-json`,
|
||||
`Docs (frontend): ${base}/api/frontend-json`,
|
||||
].join(' | ');
|
||||
logger?.log?.(msg) || console.log(msg);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AlterEventsAddSiteTrace1757000000001 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
event_id varchar(36) NOT NULL,
|
||||
event_type varchar(255) NOT NULL,
|
||||
aggregate_id varchar(255) NOT NULL,
|
||||
aggregate_type varchar(255) NOT NULL,
|
||||
site_id bigint NOT NULL DEFAULT 0,
|
||||
trace_id varchar(128) NULL,
|
||||
event_version int NOT NULL DEFAULT 1,
|
||||
event_data text NOT NULL,
|
||||
occurred_at int NOT NULL,
|
||||
processed_at int NOT NULL DEFAULT 0,
|
||||
retry_count int NOT NULL DEFAULT 0,
|
||||
next_retry_at int NOT NULL DEFAULT 0,
|
||||
last_error text NULL,
|
||||
status enum('pending','processing','processed','failed') NOT NULL DEFAULT 'pending',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_events_event_id (event_id),
|
||||
KEY idx_events_type_processed (event_type, processed_at),
|
||||
KEY idx_events_site_status (site_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
// 兼容已存在表,逐列补充
|
||||
const addColumn = async (name: string, ddl: string) => {
|
||||
try { await queryRunner.query(`ALTER TABLE events ADD COLUMN ${ddl}`); } catch { /* 已存在忽略 */ }
|
||||
};
|
||||
await addColumn('site_id', 'site_id bigint NOT NULL DEFAULT 0');
|
||||
await addColumn('trace_id', 'trace_id varchar(128) NULL');
|
||||
await addColumn('event_version', 'event_version int NOT NULL DEFAULT 1');
|
||||
await addColumn('next_retry_at', 'next_retry_at int NOT NULL DEFAULT 0');
|
||||
await addColumn('retry_count', 'retry_count int NOT NULL DEFAULT 0');
|
||||
await addColumn('last_error', 'last_error text NULL');
|
||||
// 索引
|
||||
try { await queryRunner.query(`CREATE INDEX idx_events_site_status ON events (site_id, status)`); } catch {}
|
||||
try { await queryRunner.query(`CREATE INDEX idx_events_type_processed ON events (event_type, processed_at)`); } catch {}
|
||||
try { await queryRunner.query(`ALTER TABLE events ADD UNIQUE KEY uk_events_event_id (event_id)`); } catch {}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// 仅删除新增索引/列,不删除整表
|
||||
try { await queryRunner.query(`ALTER TABLE events DROP INDEX idx_events_site_status`); } catch {}
|
||||
try { await queryRunner.query(`ALTER TABLE events DROP INDEX idx_events_type_processed`); } catch {}
|
||||
try { await queryRunner.query(`ALTER TABLE events DROP INDEX uk_events_event_id`); } catch {}
|
||||
const dropColumn = async (name: string) => {
|
||||
try { await queryRunner.query(`ALTER TABLE events DROP COLUMN ${name}`); } catch {}
|
||||
};
|
||||
await dropColumn('site_id');
|
||||
await dropColumn('trace_id');
|
||||
await dropColumn('event_version');
|
||||
await dropColumn('next_retry_at');
|
||||
await dropColumn('retry_count');
|
||||
await dropColumn('last_error');
|
||||
}
|
||||
}
|
||||
43
wwjcloud/src/vendor/event/kafka.provider.ts
vendored
43
wwjcloud/src/vendor/event/kafka.provider.ts
vendored
@@ -3,6 +3,12 @@ import { Injectable, OnModuleDestroy, Inject } from '@nestjs/common';
|
||||
interface KafkaOptions {
|
||||
clientId: string;
|
||||
brokers: string[];
|
||||
connectionTimeout?: number;
|
||||
requestTimeout?: number;
|
||||
retry?: {
|
||||
initialRetryTime?: number;
|
||||
retries?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 定义 Kafka 相关类型
|
||||
@@ -24,7 +30,6 @@ export class KafkaProvider implements OnModuleDestroy {
|
||||
constructor(
|
||||
@Inject('KAFKA_OPTIONS') private readonly kafkaOptions: KafkaOptions,
|
||||
) {
|
||||
// 添加调试日志
|
||||
console.log('🔍 KafkaProvider 配置:', {
|
||||
clientId: this.kafkaOptions.clientId,
|
||||
brokers: this.kafkaOptions.brokers,
|
||||
@@ -33,28 +38,15 @@ export class KafkaProvider implements OnModuleDestroy {
|
||||
|
||||
async ensure() {
|
||||
if (!this.kafka) {
|
||||
// 动态导入 kafkajs
|
||||
const { Kafka } = await import('kafkajs');
|
||||
|
||||
// 添加调试日志
|
||||
console.log('🔍 创建 Kafka 客户端,配置:', {
|
||||
const options: any = {
|
||||
clientId: this.kafkaOptions.clientId,
|
||||
brokers: this.kafkaOptions.brokers,
|
||||
});
|
||||
|
||||
this.kafka = new Kafka({
|
||||
clientId: this.kafkaOptions.clientId,
|
||||
brokers: this.kafkaOptions.brokers,
|
||||
// 连接配置
|
||||
connectionTimeout: 3000,
|
||||
// 请求超时
|
||||
requestTimeout: 30000,
|
||||
// 重试配置
|
||||
retry: {
|
||||
initialRetryTime: 100,
|
||||
retries: 8,
|
||||
},
|
||||
}) as KafkaClient;
|
||||
};
|
||||
if (this.kafkaOptions.connectionTimeout !== undefined) options.connectionTimeout = this.kafkaOptions.connectionTimeout;
|
||||
if (this.kafkaOptions.requestTimeout !== undefined) options.requestTimeout = this.kafkaOptions.requestTimeout;
|
||||
if (this.kafkaOptions.retry) options.retry = this.kafkaOptions.retry;
|
||||
this.kafka = new Kafka(options) as KafkaClient;
|
||||
}
|
||||
if (!this.producer) {
|
||||
this.producer = this.kafka.producer();
|
||||
@@ -75,17 +67,12 @@ export class KafkaProvider implements OnModuleDestroy {
|
||||
key: key ?? undefined,
|
||||
value:
|
||||
typeof message === 'string' ? message : JSON.stringify(message),
|
||||
headers: {
|
||||
'x-request-id': process.env.REQUEST_ID || '',
|
||||
traceparent: process.env.TRACEPARENT || '',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
// 如果发送失败,尝试重新连接
|
||||
console.error('Kafka publish error:', error);
|
||||
this.producer = null; // 重置 producer
|
||||
this.producer = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -94,9 +81,7 @@ export class KafkaProvider implements OnModuleDestroy {
|
||||
if (this.producer) {
|
||||
try {
|
||||
await this.producer.disconnect();
|
||||
} catch {
|
||||
// 忽略断开连接错误
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
wwjcloud/src/vendor/http/http.module.ts
vendored
20
wwjcloud/src/vendor/http/http.module.ts
vendored
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule as NestHttpModule } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { HttpAdapter } from './axios.adapter';
|
||||
|
||||
/**
|
||||
@@ -8,14 +9,17 @@ import { HttpAdapter } from './axios.adapter';
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
NestHttpModule.register({
|
||||
timeout: 10000,
|
||||
maxRedirects: 5,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'wwjcloud-nestjs/1.0.0',
|
||||
},
|
||||
NestHttpModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
timeout: Number(config.get<number>('http.timeout') || 10000),
|
||||
maxRedirects: Number(config.get<number>('http.maxRedirects') || 5),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'User-Agent': config.get<string>('http.userAgent') || 'wwjcloud-nestjs/1.0.0',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
providers: [HttpAdapter],
|
||||
|
||||
167
wwjcloud/src/vendor/queue/bullmq.provider.ts
vendored
167
wwjcloud/src/vendor/queue/bullmq.provider.ts
vendored
@@ -1,92 +1,147 @@
|
||||
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||
import type { Queue, QueueOptions, Job } from 'bull';
|
||||
import { ITaskQueueProvider, TaskJobOptions, TaskProcessor } from '@wwjCore/interfaces/queue.interface';
|
||||
import { ITaskQueueProvider, TaskJobOptions, TaskProcessor, ITaskQueue } from '@wwjCore/interfaces/queue.interface';
|
||||
|
||||
interface BullMQOptions {
|
||||
connection: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
password?: string;
|
||||
db?: number;
|
||||
};
|
||||
defaultJobOptions?: Record<string, any>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BullQueueProvider implements ITaskQueueProvider, OnModuleDestroy {
|
||||
private readonly queues = new Map<string, Queue>();
|
||||
private readonly queues = new Map<string, any>();
|
||||
|
||||
constructor(
|
||||
@Inject('BULLMQ_OPTIONS')
|
||||
private readonly options: Partial<QueueOptions> = {},
|
||||
private readonly options: Partial<BullMQOptions> = {},
|
||||
) {}
|
||||
|
||||
getQueue(name: string): any {
|
||||
private ensureConnection() {
|
||||
if (!this.options?.connection) {
|
||||
throw new Error('BULLMQ_OPTIONS.connection 未配置,请在配置中心(vendor.module.ts -> BULLMQ_OPTIONS)注入 Redis 连接信息');
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureQueue(name: string): Promise<any> {
|
||||
let q = this.queues.get(name);
|
||||
if (!q) {
|
||||
const Bull = require('bull');
|
||||
q = new Bull(name, {
|
||||
...this.options,
|
||||
}) as Queue;
|
||||
this.queues.set(name, q);
|
||||
this.ensureConnection();
|
||||
const { Queue, Worker } = await import('bullmq');
|
||||
const conn = this.options.connection! as any;
|
||||
const queue = new Queue(name, {
|
||||
connection: conn,
|
||||
defaultJobOptions: this.options.defaultJobOptions || {},
|
||||
});
|
||||
const worker = new Worker(
|
||||
name,
|
||||
async (job: any) => {
|
||||
const processor = (queue as any).__processors?.get(job.name) || (queue as any).__processors?.get('*');
|
||||
if (!processor) throw new Error(`No processor for job: ${job.name}`);
|
||||
const taskJob = {
|
||||
id: String(job.id),
|
||||
type: job.name,
|
||||
data: job.data,
|
||||
attemptsMade: job.attemptsMade,
|
||||
timestamp: job.timestamp,
|
||||
};
|
||||
return await processor(taskJob);
|
||||
},
|
||||
{ connection: conn }
|
||||
);
|
||||
(queue as any).__worker = worker;
|
||||
(queue as any).__processors = new Map<string, TaskProcessor<any>>();
|
||||
this.queues.set(name, queue);
|
||||
q = queue;
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
public getQueue(name: string): ITaskQueue {
|
||||
const self = this;
|
||||
return {
|
||||
async add(jobType: string, payload: any, options?: TaskJobOptions): Promise<void> {
|
||||
const q = await self.ensureQueue(name);
|
||||
const jobOptions: any = {
|
||||
attempts: options?.attempts,
|
||||
backoff: options?.backoff ? { type: options.backoff.type, delay: options.backoff.delay } : undefined,
|
||||
removeOnComplete: options?.removeOnComplete,
|
||||
removeOnFail: options?.removeOnFail,
|
||||
delay: options?.delay,
|
||||
priority: options?.priority,
|
||||
};
|
||||
await q.add(jobType, payload, jobOptions);
|
||||
},
|
||||
async addJob<T = any>(jobName: string, data: T, options?: TaskJobOptions): Promise<void> {
|
||||
return this.add(jobName, data, options);
|
||||
},
|
||||
async process(jobTypeOrProcessor: any, maybeProcessor?: any): Promise<void> {
|
||||
const q = await self.ensureQueue(name);
|
||||
const map = (q as any).__processors as Map<string, TaskProcessor<any>>;
|
||||
if (typeof jobTypeOrProcessor === 'string') {
|
||||
map.set(jobTypeOrProcessor, maybeProcessor);
|
||||
} else {
|
||||
map.set('*', jobTypeOrProcessor);
|
||||
}
|
||||
},
|
||||
async getStats(): Promise<any> {
|
||||
const q = await self.ensureQueue(name);
|
||||
const counts = await q.getJobCounts();
|
||||
return {
|
||||
waiting: counts.waiting || 0,
|
||||
active: counts.active || 0,
|
||||
completed: counts.completed || 0,
|
||||
failed: counts.failed || 0,
|
||||
delayed: counts.delayed || 0,
|
||||
};
|
||||
},
|
||||
async pause(): Promise<void> {
|
||||
const q = await self.ensureQueue(name);
|
||||
await q.pause();
|
||||
},
|
||||
async resume(): Promise<void> {
|
||||
const q = await self.ensureQueue(name);
|
||||
await q.resume();
|
||||
},
|
||||
async close(): Promise<void> {
|
||||
const q = await self.ensureQueue(name);
|
||||
const worker = (q as any).__worker;
|
||||
if (worker) await worker.close();
|
||||
await q.close();
|
||||
},
|
||||
} as ITaskQueue;
|
||||
}
|
||||
|
||||
async addJob<T = any>(queueName: string, jobName: string, data: T, options?: TaskJobOptions): Promise<void> {
|
||||
const queue = this.getQueue(queueName);
|
||||
const jobOptions: any = {
|
||||
attempts: options?.attempts ?? 3,
|
||||
backoff: options?.backoff
|
||||
? { type: options.backoff.type, delay: options.backoff.delay }
|
||||
: undefined,
|
||||
removeOnComplete: options?.removeOnComplete ?? true,
|
||||
removeOnFail: options?.removeOnFail ?? false,
|
||||
delay: options?.delay ?? 0,
|
||||
priority: options?.priority,
|
||||
};
|
||||
await queue.add(jobName, data, jobOptions);
|
||||
return this.getQueue(queueName).addJob(jobName, data, options);
|
||||
}
|
||||
|
||||
async process<T = any>(queueName: string, processor: TaskProcessor<T>): Promise<void> {
|
||||
const queue = this.getQueue(queueName);
|
||||
queue.process('*', async (job: Job<T>) => {
|
||||
const taskJob = {
|
||||
id: job.id.toString(),
|
||||
type: job.name,
|
||||
data: job.data,
|
||||
attemptsMade: job.attemptsMade,
|
||||
timestamp: job.timestamp,
|
||||
};
|
||||
return await processor(taskJob);
|
||||
});
|
||||
return this.getQueue(queueName).process(processor as any);
|
||||
}
|
||||
|
||||
async getQueueStatus(queueName: string): Promise<any> {
|
||||
const queue = this.getQueue(queueName);
|
||||
const waiting = await queue.getWaiting();
|
||||
const active = await queue.getActive();
|
||||
const completed = await queue.getCompleted();
|
||||
const failed = await queue.getFailed();
|
||||
const delayed = await queue.getDelayed();
|
||||
|
||||
return {
|
||||
waiting: waiting.length,
|
||||
active: active.length,
|
||||
completed: completed.length,
|
||||
failed: failed.length,
|
||||
delayed: delayed.length,
|
||||
};
|
||||
return this.getQueue(queueName).getStats();
|
||||
}
|
||||
|
||||
async pause(queueName: string): Promise<void> {
|
||||
const queue = this.getQueue(queueName);
|
||||
await queue.pause();
|
||||
return this.getQueue(queueName).pause();
|
||||
}
|
||||
|
||||
async resume(queueName: string): Promise<void> {
|
||||
const queue = this.getQueue(queueName);
|
||||
await queue.resume();
|
||||
return this.getQueue(queueName).resume();
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
for (const queue of this.queues.values()) {
|
||||
const stats = await this.getQueueStatus(queue.name);
|
||||
if (!stats) return false;
|
||||
for (const name of this.queues.keys()) {
|
||||
await this.getQueue(name).getStats();
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +153,8 @@ export class BullQueueProvider implements ITaskQueueProvider, OnModuleDestroy {
|
||||
async onModuleDestroy() {
|
||||
for (const q of this.queues.values()) {
|
||||
try {
|
||||
const worker = (q as any).__worker;
|
||||
if (worker) await worker.close();
|
||||
await q.close();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
6
wwjcloud/src/vendor/vendor.module.ts
vendored
6
wwjcloud/src/vendor/vendor.module.ts
vendored
@@ -5,6 +5,7 @@ import { BullQueueProvider } from './queue/bullmq.provider';
|
||||
import { KafkaProvider } from './event/kafka.provider';
|
||||
import { storageProviders } from './storage/providers/storage.provider';
|
||||
import { SysConfig } from '../common/settings/entities/sys-config.entity';
|
||||
import { RedisProvider } from './redis/redis.provider';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -15,7 +16,7 @@ import { SysConfig } from '../common/settings/entities/sys-config.entity';
|
||||
provide: 'BULLMQ_OPTIONS',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const options = {
|
||||
redis: {
|
||||
connection: {
|
||||
host: configService.get('redis.host'),
|
||||
port: configService.get('redis.port'),
|
||||
password: configService.get('redis.password'),
|
||||
@@ -49,8 +50,9 @@ import { SysConfig } from '../common/settings/entities/sys-config.entity';
|
||||
},
|
||||
BullQueueProvider,
|
||||
KafkaProvider,
|
||||
RedisProvider,
|
||||
...storageProviders,
|
||||
],
|
||||
exports: [BullQueueProvider, KafkaProvider, ...storageProviders],
|
||||
exports: [BullQueueProvider, KafkaProvider, RedisProvider, ...storageProviders],
|
||||
})
|
||||
export class VendorModule {}
|
||||
|
||||
Reference in New Issue
Block a user