| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
- import { DataSource, Repository } from 'typeorm';
- import * as cron from 'node-cron';
- import { DelaySchedulerService } from '../../src/services/delay-scheduler.service';
- import type { FeieApiConfig } from '../../src/types/feie.types';
- import { PrintTaskService } from '../../src/services/print-task.service';
- import { OrderMt } from '@d8d/orders-module-mt';
- import { FeiePrintTaskMt } from '../../src/entities';
- // Mock node-cron
- vi.mock('node-cron', () => {
- return {
- schedule: vi.fn()
- };
- });
- // Mock PrintTaskService
- vi.mock('../../src/services/print-task.service', () => {
- return {
- PrintTaskService: vi.fn()
- };
- });
- describe('DelaySchedulerService', () => {
- let service: DelaySchedulerService;
- let mockDataSource: DataSource;
- let mockPrintTaskService: PrintTaskService;
- let mockOrderRepository: Repository<OrderMt>;
- const mockFeieConfig: FeieApiConfig = {
- baseUrl: 'http://api.feieyun.cn/Api/Open/',
- user: 'test_user',
- ukey: 'test_ukey'
- };
- beforeEach(() => {
- vi.clearAllMocks();
- vi.useFakeTimers();
- // Mock PrintTaskService
- mockPrintTaskService = {
- getPendingDelayedTasks: vi.fn(),
- executePrintTask: vi.fn(),
- cancelPrintTask: vi.fn()
- } as any;
- // Mock Order Repository
- mockOrderRepository = {
- findOne: vi.fn()
- } as any;
- // Mock DataSource
- mockDataSource = {
- getRepository: vi.fn((entity) => {
- if (entity === OrderMt) {
- return mockOrderRepository;
- }
- return {} as any;
- }),
- query: vi.fn()
- } as any;
- // Mock PrintTaskService constructor
- vi.mocked(PrintTaskService).mockImplementation(() => mockPrintTaskService);
- service = new DelaySchedulerService(mockDataSource, mockFeieConfig);
- });
- afterEach(() => {
- vi.useRealTimers();
- });
- describe('constructor', () => {
- it('应该创建调度器实例', () => {
- expect(service).toBeInstanceOf(DelaySchedulerService);
- });
- it('应该设置默认延迟时间为120秒', () => {
- expect(service.getDefaultDelaySeconds()).toBe(120);
- });
- it('应该初始化PrintTaskService', () => {
- expect(PrintTaskService).toHaveBeenCalledWith(mockDataSource, mockFeieConfig);
- });
- it('应该尝试获取订单仓库', () => {
- expect(mockDataSource.getRepository).toHaveBeenCalledWith(OrderMt);
- });
- });
- describe('start', () => {
- it('应该启动调度器', async () => {
- const mockCronJob = {
- start: vi.fn(),
- stop: vi.fn()
- };
- vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
- await service.start();
- expect(cron.schedule).toHaveBeenCalledWith('*/30 * * * * *', expect.any(Function));
- expect(service.getStatus().isRunning).toBe(true);
- });
- it('应该在调度器已在运行时抛出错误', async () => {
- const mockCronJob = {
- start: vi.fn(),
- stop: vi.fn()
- };
- vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
- await service.start();
- await expect(service.start())
- .rejects
- .toThrow('调度器已经在运行中');
- });
- });
- describe('stop', () => {
- it('应该停止调度器', async () => {
- const mockCronJob = {
- start: vi.fn(),
- stop: vi.fn()
- };
- vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
- await service.start();
- await service.stop();
- expect(mockCronJob.stop).toHaveBeenCalled();
- expect(service.getStatus().isRunning).toBe(false);
- });
- it('应该在调度器未运行时抛出错误', async () => {
- await expect(service.stop())
- .rejects
- .toThrow('调度器未在运行中');
- });
- });
- describe('setDefaultDelaySeconds', () => {
- it('应该设置默认延迟时间', () => {
- service.setDefaultDelaySeconds(180);
- expect(service.getDefaultDelaySeconds()).toBe(180);
- });
- it('应该在延迟时间为负数时抛出错误', () => {
- expect(() => service.setDefaultDelaySeconds(-1))
- .toThrow('延迟时间不能为负数');
- });
- });
- describe('getStatus', () => {
- it('应该返回调度器状态', () => {
- const status = service.getStatus();
- expect(status).toEqual({
- isRunning: false,
- defaultDelaySeconds: 120,
- tenantId: null
- });
- });
- it('应该在调度器运行时返回正确状态', async () => {
- const mockCronJob = {
- start: vi.fn(),
- stop: vi.fn()
- };
- vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
- await service.start();
- const status = service.getStatus();
- expect(status.isRunning).toBe(true);
- expect(status.defaultDelaySeconds).toBe(120);
- });
- });
- describe('triggerManualProcess', () => {
- it('应该手动触发任务处理', async () => {
- const tenantId = 1;
- const mockTasks: Partial<FeiePrintTaskMt>[] = [
- {
- id: 1,
- tenantId: 1,
- taskId: 'TASK1',
- orderId: 1001,
- printerSn: 'PRINTER1',
- content: '打印内容1',
- printType: 'RECEIPT',
- printStatus: 'DELAYED',
- retryCount: 0,
- maxRetries: 3,
- scheduledAt: new Date(),
- createdAt: new Date(),
- updatedAt: new Date()
- },
- {
- id: 2,
- tenantId: 1,
- taskId: 'TASK2',
- orderId: 1002,
- printerSn: 'PRINTER2',
- content: '打印内容2',
- printType: 'RECEIPT',
- printStatus: 'DELAYED',
- retryCount: 0,
- maxRetries: 3,
- scheduledAt: new Date(),
- createdAt: new Date(),
- updatedAt: new Date()
- }
- ];
- vi.mocked(mockPrintTaskService.getPendingDelayedTasks).mockResolvedValue(mockTasks as FeiePrintTaskMt[]);
- vi.mocked(mockPrintTaskService.executePrintTask).mockResolvedValue({} as any);
- const result = await service.triggerManualProcess(tenantId);
- expect(result).toEqual({
- success: true,
- processedTasks: 2,
- message: '成功处理 2 个延迟打印任务'
- });
- expect(mockPrintTaskService.getPendingDelayedTasks).toHaveBeenCalledWith(tenantId);
- expect(mockPrintTaskService.executePrintTask).toHaveBeenCalledTimes(2);
- });
- it('应该在处理失败时返回错误信息', async () => {
- const tenantId = 1;
- vi.mocked(mockPrintTaskService.getPendingDelayedTasks).mockRejectedValue(new Error('数据库错误'));
- const result = await service.triggerManualProcess(tenantId);
- expect(result).toEqual({
- success: false,
- processedTasks: 0,
- message: '手动处理失败: 数据库错误'
- });
- });
- it('当未指定租户ID时应该返回错误', async () => {
- const result = await service.triggerManualProcess();
- expect(result).toEqual({
- success: false,
- processedTasks: 0,
- message: '未指定租户ID,无法手动处理任务'
- });
- });
- });
- describe('healthCheck', () => {
- it('应该返回健康状态', async () => {
- const health = await service.healthCheck();
- expect(health).toEqual({
- healthy: false,
- isRunning: false,
- timestamp: expect.any(Date)
- });
- });
- it('应该在调度器运行时返回健康状态', async () => {
- const mockCronJob = {
- start: vi.fn(),
- stop: vi.fn()
- };
- vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
- await service.start();
- const health = await service.healthCheck();
- expect(health).toEqual({
- healthy: true,
- isRunning: true,
- timestamp: expect.any(Date)
- });
- });
- });
- describe('shouldCancelDueToRefund', () => {
- it('当订单已退款时应该返回true', async () => {
- const tenantId = 1;
- const orderId = 1001;
- const mockOrder = {
- id: orderId,
- tenantId,
- payState: 3, // 已退款
- state: 1
- };
- vi.mocked(mockOrderRepository.findOne).mockResolvedValue(mockOrder as any);
- // 通过私有方法测试
- const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
- expect(result).toBe(true);
- expect(mockOrderRepository.findOne).toHaveBeenCalledWith({
- where: { id: orderId, tenantId }
- });
- });
- it('当订单已关闭时应该返回true', async () => {
- const tenantId = 1;
- const orderId = 1001;
- const mockOrder = {
- id: orderId,
- tenantId,
- payState: 2, // 已支付
- state: 5 // 订单关闭
- };
- vi.mocked(mockOrderRepository.findOne).mockResolvedValue(mockOrder as any);
- const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
- expect(result).toBe(true);
- });
- it('当订单正常时应该返回false', async () => {
- const tenantId = 1;
- const orderId = 1001;
- const mockOrder = {
- id: orderId,
- tenantId,
- payState: 2, // 已支付
- state: 1 // 正常
- };
- vi.mocked(mockOrderRepository.findOne).mockResolvedValue(mockOrder as any);
- const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
- expect(result).toBe(false);
- });
- it('当订单不存在时应该返回true', async () => {
- const tenantId = 1;
- const orderId = 1001;
- vi.mocked(mockOrderRepository.findOne).mockResolvedValue(null);
- const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
- expect(result).toBe(true);
- });
- it('当订单仓库不可用时应该返回false', async () => {
- const tenantId = 1;
- const orderId = 1001;
- // 模拟订单仓库不可用
- (service as any).orderRepository = null;
- const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
- expect(result).toBe(false);
- });
- });
- describe('processSingleDelayedTask', () => {
- it('一次调度周期内每个打印任务应该只执行一次', async () => {
- const tenantId = 1;
- const mockTask: Partial<FeiePrintTaskMt> = {
- id: 1,
- tenantId: 1,
- taskId: 'TASK1',
- orderId: 1001,
- printerSn: 'PRINTER1',
- content: '打印内容',
- printType: 'RECEIPT',
- printStatus: 'DELAYED',
- retryCount: 0,
- maxRetries: 3,
- scheduledAt: new Date(),
- createdAt: new Date(),
- updatedAt: new Date()
- };
- // Mock 订单状态正常(未退款)
- vi.mocked(mockOrderRepository.findOne).mockResolvedValue({
- id: 1001,
- tenantId: 1,
- payState: 2, // 已支付
- state: 1 // 正常
- } as any);
- // Mock executePrintTask 成功
- vi.mocked(mockPrintTaskService.executePrintTask).mockResolvedValue({} as any);
- // 调用私有方法 processSingleDelayedTask
- await (service as any).processSingleDelayedTask(tenantId, mockTask);
- // 验证 executePrintTask 只被调用了一次
- expect(mockPrintTaskService.executePrintTask).toHaveBeenCalledTimes(1);
- expect(mockPrintTaskService.executePrintTask).toHaveBeenCalledWith(tenantId, 'TASK1');
- // 验证订单状态检查被调用
- expect(mockOrderRepository.findOne).toHaveBeenCalledWith({
- where: { id: 1001, tenantId: 1 }
- });
- });
- it('当任务状态为最终状态时应该跳过执行', async () => {
- const tenantId = 1;
- // 测试各种最终状态
- const finalStatuses = ['SUCCESS', 'FAILED', 'CANCELLED'];
- for (const status of finalStatuses) {
- const mockTask: Partial<FeiePrintTaskMt> = {
- id: 1,
- tenantId: 1,
- taskId: 'TASK1',
- orderId: 1001,
- printerSn: 'PRINTER1',
- content: '打印内容',
- printType: 'RECEIPT',
- printStatus: status,
- retryCount: 0,
- maxRetries: 3,
- scheduledAt: new Date(),
- createdAt: new Date(),
- updatedAt: new Date()
- };
- // 重置mock调用计数
- vi.mocked(mockPrintTaskService.executePrintTask).mockClear();
- // 调用私有方法 processSingleDelayedTask
- await (service as any).processSingleDelayedTask(tenantId, mockTask);
- // 验证 executePrintTask 没有被调用(因为任务已经是最终状态)
- expect(mockPrintTaskService.executePrintTask).not.toHaveBeenCalled();
- }
- });
- it('当订单已退款时应该取消打印任务', async () => {
- const tenantId = 1;
- const mockTask: Partial<FeiePrintTaskMt> = {
- id: 1,
- tenantId: 1,
- taskId: 'TASK1',
- orderId: 1001,
- printerSn: 'PRINTER1',
- content: '打印内容',
- printType: 'RECEIPT',
- printStatus: 'DELAYED',
- retryCount: 0,
- maxRetries: 3,
- scheduledAt: new Date(),
- createdAt: new Date(),
- updatedAt: new Date()
- };
- // Mock 订单状态为已退款
- vi.mocked(mockOrderRepository.findOne).mockResolvedValue({
- id: 1001,
- tenantId: 1,
- payState: 3, // 已退款
- state: 1
- } as any);
- // Mock cancelPrintTask
- vi.mocked(mockPrintTaskService.cancelPrintTask).mockResolvedValue({} as any);
- // 调用私有方法 processSingleDelayedTask
- await (service as any).processSingleDelayedTask(tenantId, mockTask);
- // 验证 cancelPrintTask 被调用,而不是 executePrintTask
- expect(mockPrintTaskService.cancelPrintTask).toHaveBeenCalledTimes(1);
- expect(mockPrintTaskService.cancelPrintTask).toHaveBeenCalledWith(tenantId, 'TASK1', 'REFUND');
- expect(mockPrintTaskService.executePrintTask).not.toHaveBeenCalled();
- });
- });
- });
|