feat: 完成 NestJS 后端核心底座开发 (M1-M6) 和 Ant Design Vue 前端迁移

主要更新:
1. 后端核心底座完成 (M1-M6):
   - 健康检查、指标监控、分布式锁
   - 事件总线、队列系统、事务管理
   - 安全守卫、多租户隔离、存储适配器
   - 审计日志、配置管理、多语言支持

2. 前端迁移到 Ant Design Vue:
   - 从 Element Plus 迁移到 Ant Design Vue
   - 完善 system 模块 (role/menu/dept)
   - 修复依赖和配置问题

3. 文档完善:
   - AI 开发工作流文档
   - 架构约束和开发规范
   - 项目进度跟踪

4. 其他改进:
   - 修复编译错误和类型问题
   - 完善测试用例
   - 优化项目结构
This commit is contained in:
万物街
2025-08-27 11:24:22 +08:00
parent be07b9ffec
commit 1cd5d3bdef
696 changed files with 36708 additions and 16868 deletions

View File

@@ -0,0 +1,232 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { TestModule } from '../test.module';
import { TestService } from '../test.service';
import { UnifiedQueueService } from '../../src/core/queue/unified-queue.service';
import { DatabaseQueueProvider } from '../../src/core/queue/database-queue.provider';
import { QueueModule } from '../../src/core/queue/queue.module';
describe('Queue System (e2e)', () => {
let app: INestApplication;
let testService: TestService;
let unifiedQueueService: UnifiedQueueService;
let databaseQueueProvider: DatabaseQueueProvider;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TestModule, QueueModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
testService = moduleFixture.get<TestService>(TestService);
unifiedQueueService = moduleFixture.get<UnifiedQueueService>(UnifiedQueueService);
databaseQueueProvider = moduleFixture.get<DatabaseQueueProvider>(DatabaseQueueProvider);
});
afterAll(async () => {
if (app) {
await app.close();
}
});
describe('Test Controller Endpoints', () => {
it('/test/status (GET) - should return service status', () => {
return request(app.getHttpServer())
.get('/test/status')
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('message');
expect(res.body).toHaveProperty('timestamp');
expect(res.body).toHaveProperty('services');
expect(res.body.services).toHaveProperty('redis');
expect(res.body.services).toHaveProperty('kafka');
});
});
it('/test/kafka (POST) - should publish event to Kafka', () => {
const testData = { test: 'kafka-event', value: 123 };
return request(app.getHttpServer())
.post('/test/kafka')
.send(testData)
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('success', true);
expect(res.body).toHaveProperty('message');
expect(res.body).toHaveProperty('topic', 'test-topic');
expect(res.body).toHaveProperty('data', testData);
});
});
it('/test/redis (POST) - should enqueue job to Redis', () => {
const testData = { test: 'redis-job', value: 456 };
return request(app.getHttpServer())
.post('/test/redis')
.send(testData)
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('success', true);
expect(res.body).toHaveProperty('message');
expect(res.body).toHaveProperty('jobId');
expect(res.body).toHaveProperty('data', testData);
});
});
});
describe('UnifiedQueueService', () => {
it('should be defined', () => {
expect(unifiedQueueService).toBeDefined();
});
it('should add task to queue', async () => {
const result = await unifiedQueueService.addTask('test-queue', {
data: { test: 'data' },
priority: 1,
delay: 0,
attempts: 3,
});
expect(result).toBeDefined();
});
it('should process task from queue', async () => {
let processedData: any = null;
await unifiedQueueService.processTask('test-queue', async (job: any) => {
processedData = job.data;
return { success: true };
});
// Add a task to be processed
await unifiedQueueService.addTask('test-queue', {
data: { test: 'process-data' },
priority: 1,
});
// Wait a bit for processing
await new Promise(resolve => setTimeout(resolve, 1000));
expect(processedData).toBeDefined();
});
it('should publish event', async () => {
const event = {
eventType: 'test.event',
aggregateId: 'test-123',
aggregateType: 'Test',
version: '1.0',
occurredAt: new Date().toISOString(),
tenantId: 'tenant-1',
idempotencyKey: 'key-123',
traceId: 'trace-123',
data: { test: 'event-data' },
};
await expect(unifiedQueueService.publishEvent(event)).resolves.not.toThrow();
});
});
describe('DatabaseQueueProvider', () => {
it('should be defined', () => {
expect(databaseQueueProvider).toBeDefined();
});
it('should add job to database queue', async () => {
const jobData = {
type: 'test-job',
payload: { test: 'database-job' },
options: {
priority: 1,
delay: 0,
attempts: 3,
},
};
const result = await databaseQueueProvider.add('test-db-queue', jobData.type, jobData.payload, jobData.options);
expect(result).toBeDefined();
});
it('should publish event to database', async () => {
const event = {
eventType: 'test.database.event',
aggregateId: 'db-test-123',
aggregateType: 'DatabaseTest',
version: '1.0',
occurredAt: new Date().toISOString(),
tenantId: 'tenant-1',
idempotencyKey: 'db-key-123',
traceId: 'db-trace-123',
data: { test: 'database-event-data' },
};
await expect(databaseQueueProvider.publish(event)).resolves.not.toThrow();
});
});
describe('Service Integration', () => {
it('should have all required services available', () => {
expect(testService).toBeDefined();
expect(unifiedQueueService).toBeDefined();
expect(databaseQueueProvider).toBeDefined();
});
});
describe('Integration Tests', () => {
it('should handle complete queue workflow', async () => {
// Test the complete workflow: add task -> process task -> publish event
const taskData = { workflow: 'test', step: 1 };
// Add task
const taskResult = await unifiedQueueService.addTask('workflow-queue', {
data: taskData,
priority: 1,
});
expect(taskResult).toBeDefined();
// Process task and publish event
await unifiedQueueService.processTask('workflow-queue', async (job: any) => {
const event = {
eventType: 'workflow.completed',
aggregateId: 'workflow-123',
aggregateType: 'Workflow',
version: '1.0',
occurredAt: new Date().toISOString(),
tenantId: 'tenant-1',
idempotencyKey: 'workflow-key-123',
traceId: 'workflow-trace-123',
data: job.data,
};
await unifiedQueueService.publishEvent(event);
return { success: true, processed: job.data };
});
// Wait for processing
await new Promise(resolve => setTimeout(resolve, 1000));
});
it('should handle error scenarios gracefully', async () => {
// Test error handling in task processing
await unifiedQueueService.processTask('error-queue', async (job: any) => {
if (job.data.shouldFail) {
throw new Error('Intentional test error');
}
return { success: true };
});
// Add a failing task
await unifiedQueueService.addTask('error-queue', {
data: { shouldFail: true },
priority: 1,
attempts: 1, // Only try once
});
// Wait for processing attempt
await new Promise(resolve => setTimeout(resolve, 1000));
// The test passes if no unhandled errors are thrown
expect(true).toBe(true);
});
});
});

View File

@@ -0,0 +1,340 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UnifiedQueueService } from '../../src/core/queue/unified-queue.service';
import { DatabaseQueueProvider } from '../../src/core/queue/database-queue.provider';
import { Logger } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JobEntity } from '../../src/core/queue/entities/job.entity';
import { EventEntity } from '../../src/core/queue/entities/event.entity';
import { TASK_QUEUE_PROVIDER, EVENT_BUS_PROVIDER } from '../../src/core/interfaces/queue.interface';
describe('Queue System Unit Tests', () => {
let unifiedQueueService: UnifiedQueueService;
let databaseQueueProvider: DatabaseQueueProvider;
let mockJobRepository: jest.Mocked<Repository<JobEntity>>;
let mockEventRepository: jest.Mocked<Repository<EventEntity>>;
beforeEach(async () => {
// Create mock repositories
mockJobRepository = {
create: jest.fn(),
save: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(),
} as any;
mockEventRepository = {
create: jest.fn(),
save: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(),
} as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
UnifiedQueueService,
DatabaseQueueProvider,
{
provide: getRepositoryToken(JobEntity),
useValue: mockJobRepository,
},
{
provide: getRepositoryToken(EventEntity),
useValue: mockEventRepository,
},
{
provide: TASK_QUEUE_PROVIDER,
useExisting: DatabaseQueueProvider,
},
{
provide: EVENT_BUS_PROVIDER,
useExisting: DatabaseQueueProvider,
},
{
provide: Logger,
useValue: {
log: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
},
},
],
}).compile();
unifiedQueueService = module.get<UnifiedQueueService>(UnifiedQueueService);
databaseQueueProvider = module.get<DatabaseQueueProvider>(DatabaseQueueProvider);
});
describe('UnifiedQueueService', () => {
it('should be defined', () => {
expect(unifiedQueueService).toBeDefined();
});
it('should have all required methods', () => {
expect(typeof unifiedQueueService.addTask).toBe('function');
expect(typeof unifiedQueueService.processTask).toBe('function');
expect(typeof unifiedQueueService.publishEvent).toBe('function');
expect(typeof unifiedQueueService.publishEvents).toBe('function');
expect(typeof unifiedQueueService.getQueueStatus).toBe('function');
expect(typeof unifiedQueueService.pauseTaskQueue).toBe('function');
expect(typeof unifiedQueueService.resumeTaskQueue).toBe('function');
expect(typeof unifiedQueueService.cleanTaskQueue).toBe('function');
expect(typeof unifiedQueueService.close).toBe('function');
});
it('should validate task options', () => {
const validOptions = {
data: { test: 'data' },
priority: 1,
delay: 0,
attempts: 3,
};
expect(() => {
// This should not throw
const options = validOptions;
expect(options.data).toBeDefined();
expect(typeof options.priority).toBe('number');
expect(typeof options.delay).toBe('number');
expect(typeof options.attempts).toBe('number');
}).not.toThrow();
});
it('should validate event structure', () => {
const validEvent = {
eventType: 'test.event',
aggregateId: 'test-123',
aggregateType: 'Test',
version: '1.0',
occurredAt: new Date().toISOString(),
tenantId: 'tenant-1',
idempotencyKey: 'key-123',
traceId: 'trace-123',
data: { test: 'data' },
};
expect(validEvent.eventType).toBeDefined();
expect(validEvent.aggregateId).toBeDefined();
expect(validEvent.aggregateType).toBeDefined();
expect(validEvent.version).toBeDefined();
expect(validEvent.occurredAt).toBeDefined();
expect(validEvent.tenantId).toBeDefined();
expect(validEvent.idempotencyKey).toBeDefined();
expect(validEvent.traceId).toBeDefined();
expect(validEvent.data).toBeDefined();
});
});
describe('DatabaseQueueProvider', () => {
it('should be defined', () => {
expect(databaseQueueProvider).toBeDefined();
});
it('should have all required methods', () => {
expect(typeof databaseQueueProvider.add).toBe('function');
expect(typeof databaseQueueProvider.process).toBe('function');
expect(typeof databaseQueueProvider.getStatus).toBe('function');
expect(typeof databaseQueueProvider.pause).toBe('function');
expect(typeof databaseQueueProvider.resume).toBe('function');
expect(typeof databaseQueueProvider.clean).toBe('function');
expect(typeof databaseQueueProvider.publish).toBe('function');
expect(typeof databaseQueueProvider.subscribe).toBe('function');
expect(typeof databaseQueueProvider.close).toBe('function');
});
it('should create job entity correctly', async () => {
const mockJob = {
id: 1,
queue_name: 'test-queue',
job_type: 'test-job',
payload: { test: 'data' },
status: 'pending',
priority: 1,
attempts: 0,
max_attempts: 3,
created_at: Date.now(),
updated_at: Date.now(),
scheduled_at: Date.now(),
processed_at: null,
failed_at: null,
error_message: null,
};
mockJobRepository.create.mockReturnValue(mockJob as any);
mockJobRepository.save.mockResolvedValue(mockJob as any);
const result = await databaseQueueProvider.add('test-queue', 'test-job', { test: 'data' }, {
priority: 1,
delay: 0,
attempts: 3,
});
expect(mockJobRepository.create).toHaveBeenCalled();
expect(mockJobRepository.save).toHaveBeenCalled();
expect(result).toBeDefined();
});
it('should create event entity correctly', async () => {
const mockEvent = {
id: 1,
event_type: 'test.event',
aggregate_id: 'test-123',
aggregate_type: 'Test',
version: '1.0',
occurred_at: new Date().toISOString(),
tenant_id: 'tenant-1',
idempotency_key: 'key-123',
trace_id: 'trace-123',
data: { test: 'data' },
created_at: Date.now(),
};
mockEventRepository.create.mockReturnValue(mockEvent as any);
mockEventRepository.save.mockResolvedValue(mockEvent as any);
const event = {
eventType: 'test.event',
aggregateId: 'test-123',
aggregateType: 'Test',
version: '1.0',
occurredAt: new Date().toISOString(),
tenantId: 'tenant-1',
idempotencyKey: 'key-123',
traceId: 'trace-123',
data: { test: 'data' },
};
await databaseQueueProvider.publish(event);
expect(mockEventRepository.create).toHaveBeenCalled();
expect(mockEventRepository.save).toHaveBeenCalled();
});
});
describe('Service Integration', () => {
it('should have all required services available', () => {
expect(unifiedQueueService).toBeDefined();
expect(databaseQueueProvider).toBeDefined();
});
});
describe('Error Handling', () => {
it('should handle database connection errors gracefully', async () => {
mockJobRepository.save.mockRejectedValue(new Error('Database connection failed'));
try {
await databaseQueueProvider.add('test-queue', 'test-job', { test: 'data' }, {
priority: 1,
delay: 0,
attempts: 3,
});
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('Database connection failed');
}
});
it('should handle invalid event data', () => {
const invalidEvent = {
// Missing required fields
eventType: 'test.event',
// aggregateId: missing
// aggregateType: missing
version: '1.0',
occurredAt: new Date().toISOString(),
data: { test: 'data' },
};
// Test validation logic
expect(invalidEvent.eventType).toBeDefined();
expect(invalidEvent.version).toBeDefined();
expect(invalidEvent.occurredAt).toBeDefined();
expect(invalidEvent.data).toBeDefined();
// These should be undefined, indicating invalid event
expect((invalidEvent as any).aggregateId).toBeUndefined();
expect((invalidEvent as any).aggregateType).toBeUndefined();
});
it('should handle invalid task options', () => {
const invalidOptions = {
data: { test: 'data' },
priority: 'high', // Should be number
delay: -1, // Should be non-negative
attempts: 0, // Should be positive
};
// Test validation logic
expect(typeof invalidOptions.priority).toBe('string'); // Invalid
expect(invalidOptions.delay).toBeLessThan(0); // Invalid
expect(invalidOptions.attempts).toBe(0); // Invalid
});
});
describe('Performance and Scalability', () => {
it('should handle multiple concurrent operations', async () => {
const operations = [];
// Simulate multiple concurrent task additions
for (let i = 0; i < 10; i++) {
mockJobRepository.save.mockResolvedValueOnce({
id: i,
queue_name: 'concurrent-queue',
job_type: 'concurrent-job',
payload: { index: i },
status: 'pending',
} as any);
operations.push(
databaseQueueProvider.add('concurrent-queue', 'concurrent-job', { index: i }, {
priority: 1,
delay: 0,
attempts: 3,
})
);
}
const results = await Promise.all(operations);
expect(results).toHaveLength(10);
expect(mockJobRepository.save).toHaveBeenCalledTimes(10);
});
it('should handle batch event publishing', async () => {
const events = [];
for (let i = 0; i < 5; i++) {
events.push({
eventType: 'batch.event',
aggregateId: `batch-${i}`,
aggregateType: 'Batch',
version: '1.0',
occurredAt: new Date().toISOString(),
tenantId: 'tenant-1',
idempotencyKey: `batch-key-${i}`,
traceId: `batch-trace-${i}`,
data: { index: i },
});
mockEventRepository.save.mockResolvedValueOnce({
id: i,
event_type: 'batch.event',
aggregate_id: `batch-${i}`,
data: { index: i },
} as any);
}
// Test batch publishing
const publishPromises = events.map(event => databaseQueueProvider.publish(event));
await Promise.all(publishPromises);
expect(mockEventRepository.save).toHaveBeenCalledTimes(5);
});
});
});