delay-scheduler.service.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  2. import { DataSource, Repository } from 'typeorm';
  3. import * as cron from 'node-cron';
  4. import { DelaySchedulerService } from '../../src/services/delay-scheduler.service';
  5. import type { FeieApiConfig } from '../../src/types/feie.types';
  6. import { PrintTaskService } from '../../src/services/print-task.service';
  7. import { OrderMt } from '@d8d/orders-module-mt';
  8. import { FeiePrintTaskMt } from '../../src/entities';
  9. // Mock node-cron
  10. vi.mock('node-cron', () => {
  11. return {
  12. schedule: vi.fn()
  13. };
  14. });
  15. // Mock PrintTaskService
  16. vi.mock('../../src/services/print-task.service', () => {
  17. return {
  18. PrintTaskService: vi.fn()
  19. };
  20. });
  21. describe('DelaySchedulerService', () => {
  22. let service: DelaySchedulerService;
  23. let mockDataSource: DataSource;
  24. let mockPrintTaskService: PrintTaskService;
  25. let mockOrderRepository: Repository<OrderMt>;
  26. const mockFeieConfig: FeieApiConfig = {
  27. baseUrl: 'http://api.feieyun.cn/Api/Open/',
  28. user: 'test_user',
  29. ukey: 'test_ukey'
  30. };
  31. beforeEach(() => {
  32. vi.clearAllMocks();
  33. vi.useFakeTimers();
  34. // Mock PrintTaskService
  35. mockPrintTaskService = {
  36. getPendingDelayedTasks: vi.fn(),
  37. executePrintTask: vi.fn(),
  38. cancelPrintTask: vi.fn()
  39. } as any;
  40. // Mock Order Repository
  41. mockOrderRepository = {
  42. findOne: vi.fn()
  43. } as any;
  44. // Mock DataSource
  45. mockDataSource = {
  46. getRepository: vi.fn((entity) => {
  47. if (entity === OrderMt) {
  48. return mockOrderRepository;
  49. }
  50. return {} as any;
  51. }),
  52. query: vi.fn()
  53. } as any;
  54. // Mock PrintTaskService constructor
  55. vi.mocked(PrintTaskService).mockImplementation(() => mockPrintTaskService);
  56. service = new DelaySchedulerService(mockDataSource, mockFeieConfig);
  57. });
  58. afterEach(() => {
  59. vi.useRealTimers();
  60. });
  61. describe('constructor', () => {
  62. it('应该创建调度器实例', () => {
  63. expect(service).toBeInstanceOf(DelaySchedulerService);
  64. });
  65. it('应该设置默认延迟时间为120秒', () => {
  66. expect(service.getDefaultDelaySeconds()).toBe(120);
  67. });
  68. it('应该初始化PrintTaskService', () => {
  69. expect(PrintTaskService).toHaveBeenCalledWith(mockDataSource, mockFeieConfig);
  70. });
  71. it('应该尝试获取订单仓库', () => {
  72. expect(mockDataSource.getRepository).toHaveBeenCalledWith(OrderMt);
  73. });
  74. });
  75. describe('start', () => {
  76. it('应该启动调度器', async () => {
  77. const mockCronJob = {
  78. start: vi.fn(),
  79. stop: vi.fn()
  80. };
  81. vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
  82. await service.start();
  83. expect(cron.schedule).toHaveBeenCalledWith('*/30 * * * * *', expect.any(Function));
  84. expect(service.getStatus().isRunning).toBe(true);
  85. });
  86. it('应该在调度器已在运行时抛出错误', async () => {
  87. const mockCronJob = {
  88. start: vi.fn(),
  89. stop: vi.fn()
  90. };
  91. vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
  92. await service.start();
  93. await expect(service.start())
  94. .rejects
  95. .toThrow('调度器已经在运行中');
  96. });
  97. });
  98. describe('stop', () => {
  99. it('应该停止调度器', async () => {
  100. const mockCronJob = {
  101. start: vi.fn(),
  102. stop: vi.fn()
  103. };
  104. vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
  105. await service.start();
  106. await service.stop();
  107. expect(mockCronJob.stop).toHaveBeenCalled();
  108. expect(service.getStatus().isRunning).toBe(false);
  109. });
  110. it('应该在调度器未运行时抛出错误', async () => {
  111. await expect(service.stop())
  112. .rejects
  113. .toThrow('调度器未在运行中');
  114. });
  115. });
  116. describe('setDefaultDelaySeconds', () => {
  117. it('应该设置默认延迟时间', () => {
  118. service.setDefaultDelaySeconds(180);
  119. expect(service.getDefaultDelaySeconds()).toBe(180);
  120. });
  121. it('应该在延迟时间为负数时抛出错误', () => {
  122. expect(() => service.setDefaultDelaySeconds(-1))
  123. .toThrow('延迟时间不能为负数');
  124. });
  125. });
  126. describe('getStatus', () => {
  127. it('应该返回调度器状态', () => {
  128. const status = service.getStatus();
  129. expect(status).toEqual({
  130. isRunning: false,
  131. defaultDelaySeconds: 120,
  132. tenantId: null
  133. });
  134. });
  135. it('应该在调度器运行时返回正确状态', async () => {
  136. const mockCronJob = {
  137. start: vi.fn(),
  138. stop: vi.fn()
  139. };
  140. vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
  141. await service.start();
  142. const status = service.getStatus();
  143. expect(status.isRunning).toBe(true);
  144. expect(status.defaultDelaySeconds).toBe(120);
  145. });
  146. });
  147. describe('triggerManualProcess', () => {
  148. it('应该手动触发任务处理', async () => {
  149. const tenantId = 1;
  150. const mockTasks: Partial<FeiePrintTaskMt>[] = [
  151. {
  152. id: 1,
  153. tenantId: 1,
  154. taskId: 'TASK1',
  155. orderId: 1001,
  156. printerSn: 'PRINTER1',
  157. content: '打印内容1',
  158. printType: 'RECEIPT',
  159. printStatus: 'DELAYED',
  160. retryCount: 0,
  161. maxRetries: 3,
  162. scheduledAt: new Date(),
  163. createdAt: new Date(),
  164. updatedAt: new Date()
  165. },
  166. {
  167. id: 2,
  168. tenantId: 1,
  169. taskId: 'TASK2',
  170. orderId: 1002,
  171. printerSn: 'PRINTER2',
  172. content: '打印内容2',
  173. printType: 'RECEIPT',
  174. printStatus: 'DELAYED',
  175. retryCount: 0,
  176. maxRetries: 3,
  177. scheduledAt: new Date(),
  178. createdAt: new Date(),
  179. updatedAt: new Date()
  180. }
  181. ];
  182. vi.mocked(mockPrintTaskService.getPendingDelayedTasks).mockResolvedValue(mockTasks as FeiePrintTaskMt[]);
  183. vi.mocked(mockPrintTaskService.executePrintTask).mockResolvedValue({} as any);
  184. const result = await service.triggerManualProcess(tenantId);
  185. expect(result).toEqual({
  186. success: true,
  187. processedTasks: 2,
  188. message: '成功处理 2 个延迟打印任务'
  189. });
  190. expect(mockPrintTaskService.getPendingDelayedTasks).toHaveBeenCalledWith(tenantId);
  191. expect(mockPrintTaskService.executePrintTask).toHaveBeenCalledTimes(2);
  192. });
  193. it('应该在处理失败时返回错误信息', async () => {
  194. const tenantId = 1;
  195. vi.mocked(mockPrintTaskService.getPendingDelayedTasks).mockRejectedValue(new Error('数据库错误'));
  196. const result = await service.triggerManualProcess(tenantId);
  197. expect(result).toEqual({
  198. success: false,
  199. processedTasks: 0,
  200. message: '手动处理失败: 数据库错误'
  201. });
  202. });
  203. it('当未指定租户ID时应该返回错误', async () => {
  204. const result = await service.triggerManualProcess();
  205. expect(result).toEqual({
  206. success: false,
  207. processedTasks: 0,
  208. message: '未指定租户ID,无法手动处理任务'
  209. });
  210. });
  211. });
  212. describe('healthCheck', () => {
  213. it('应该返回健康状态', async () => {
  214. const health = await service.healthCheck();
  215. expect(health).toEqual({
  216. healthy: false,
  217. isRunning: false,
  218. timestamp: expect.any(Date)
  219. });
  220. });
  221. it('应该在调度器运行时返回健康状态', async () => {
  222. const mockCronJob = {
  223. start: vi.fn(),
  224. stop: vi.fn()
  225. };
  226. vi.mocked(cron.schedule).mockReturnValue(mockCronJob as any);
  227. await service.start();
  228. const health = await service.healthCheck();
  229. expect(health).toEqual({
  230. healthy: true,
  231. isRunning: true,
  232. timestamp: expect.any(Date)
  233. });
  234. });
  235. });
  236. describe('shouldCancelDueToRefund', () => {
  237. it('当订单已退款时应该返回true', async () => {
  238. const tenantId = 1;
  239. const orderId = 1001;
  240. const mockOrder = {
  241. id: orderId,
  242. tenantId,
  243. payState: 3, // 已退款
  244. state: 1
  245. };
  246. vi.mocked(mockOrderRepository.findOne).mockResolvedValue(mockOrder as any);
  247. // 通过私有方法测试
  248. const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
  249. expect(result).toBe(true);
  250. expect(mockOrderRepository.findOne).toHaveBeenCalledWith({
  251. where: { id: orderId, tenantId }
  252. });
  253. });
  254. it('当订单已关闭时应该返回true', async () => {
  255. const tenantId = 1;
  256. const orderId = 1001;
  257. const mockOrder = {
  258. id: orderId,
  259. tenantId,
  260. payState: 2, // 已支付
  261. state: 5 // 订单关闭
  262. };
  263. vi.mocked(mockOrderRepository.findOne).mockResolvedValue(mockOrder as any);
  264. const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
  265. expect(result).toBe(true);
  266. });
  267. it('当订单正常时应该返回false', async () => {
  268. const tenantId = 1;
  269. const orderId = 1001;
  270. const mockOrder = {
  271. id: orderId,
  272. tenantId,
  273. payState: 2, // 已支付
  274. state: 1 // 正常
  275. };
  276. vi.mocked(mockOrderRepository.findOne).mockResolvedValue(mockOrder as any);
  277. const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
  278. expect(result).toBe(false);
  279. });
  280. it('当订单不存在时应该返回true', async () => {
  281. const tenantId = 1;
  282. const orderId = 1001;
  283. vi.mocked(mockOrderRepository.findOne).mockResolvedValue(null);
  284. const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
  285. expect(result).toBe(true);
  286. });
  287. it('当订单仓库不可用时应该返回false', async () => {
  288. const tenantId = 1;
  289. const orderId = 1001;
  290. // 模拟订单仓库不可用
  291. (service as any).orderRepository = null;
  292. const result = await (service as any).shouldCancelDueToRefund(tenantId, orderId);
  293. expect(result).toBe(false);
  294. });
  295. });
  296. describe('processSingleDelayedTask', () => {
  297. it('一次调度周期内每个打印任务应该只执行一次', async () => {
  298. const tenantId = 1;
  299. const mockTask: Partial<FeiePrintTaskMt> = {
  300. id: 1,
  301. tenantId: 1,
  302. taskId: 'TASK1',
  303. orderId: 1001,
  304. printerSn: 'PRINTER1',
  305. content: '打印内容',
  306. printType: 'RECEIPT',
  307. printStatus: 'DELAYED',
  308. retryCount: 0,
  309. maxRetries: 3,
  310. scheduledAt: new Date(),
  311. createdAt: new Date(),
  312. updatedAt: new Date()
  313. };
  314. // Mock 订单状态正常(未退款)
  315. vi.mocked(mockOrderRepository.findOne).mockResolvedValue({
  316. id: 1001,
  317. tenantId: 1,
  318. payState: 2, // 已支付
  319. state: 1 // 正常
  320. } as any);
  321. // Mock executePrintTask 成功
  322. vi.mocked(mockPrintTaskService.executePrintTask).mockResolvedValue({} as any);
  323. // 调用私有方法 processSingleDelayedTask
  324. await (service as any).processSingleDelayedTask(tenantId, mockTask);
  325. // 验证 executePrintTask 只被调用了一次
  326. expect(mockPrintTaskService.executePrintTask).toHaveBeenCalledTimes(1);
  327. expect(mockPrintTaskService.executePrintTask).toHaveBeenCalledWith(tenantId, 'TASK1');
  328. // 验证订单状态检查被调用
  329. expect(mockOrderRepository.findOne).toHaveBeenCalledWith({
  330. where: { id: 1001, tenantId: 1 }
  331. });
  332. });
  333. it('当任务状态为最终状态时应该跳过执行', async () => {
  334. const tenantId = 1;
  335. // 测试各种最终状态
  336. const finalStatuses = ['SUCCESS', 'FAILED', 'CANCELLED'];
  337. for (const status of finalStatuses) {
  338. const mockTask: Partial<FeiePrintTaskMt> = {
  339. id: 1,
  340. tenantId: 1,
  341. taskId: 'TASK1',
  342. orderId: 1001,
  343. printerSn: 'PRINTER1',
  344. content: '打印内容',
  345. printType: 'RECEIPT',
  346. printStatus: status,
  347. retryCount: 0,
  348. maxRetries: 3,
  349. scheduledAt: new Date(),
  350. createdAt: new Date(),
  351. updatedAt: new Date()
  352. };
  353. // 重置mock调用计数
  354. vi.mocked(mockPrintTaskService.executePrintTask).mockClear();
  355. // 调用私有方法 processSingleDelayedTask
  356. await (service as any).processSingleDelayedTask(tenantId, mockTask);
  357. // 验证 executePrintTask 没有被调用(因为任务已经是最终状态)
  358. expect(mockPrintTaskService.executePrintTask).not.toHaveBeenCalled();
  359. }
  360. });
  361. it('当订单已退款时应该取消打印任务', async () => {
  362. const tenantId = 1;
  363. const mockTask: Partial<FeiePrintTaskMt> = {
  364. id: 1,
  365. tenantId: 1,
  366. taskId: 'TASK1',
  367. orderId: 1001,
  368. printerSn: 'PRINTER1',
  369. content: '打印内容',
  370. printType: 'RECEIPT',
  371. printStatus: 'DELAYED',
  372. retryCount: 0,
  373. maxRetries: 3,
  374. scheduledAt: new Date(),
  375. createdAt: new Date(),
  376. updatedAt: new Date()
  377. };
  378. // Mock 订单状态为已退款
  379. vi.mocked(mockOrderRepository.findOne).mockResolvedValue({
  380. id: 1001,
  381. tenantId: 1,
  382. payState: 3, // 已退款
  383. state: 1
  384. } as any);
  385. // Mock cancelPrintTask
  386. vi.mocked(mockPrintTaskService.cancelPrintTask).mockResolvedValue({} as any);
  387. // 调用私有方法 processSingleDelayedTask
  388. await (service as any).processSingleDelayedTask(tenantId, mockTask);
  389. // 验证 cancelPrintTask 被调用,而不是 executePrintTask
  390. expect(mockPrintTaskService.cancelPrintTask).toHaveBeenCalledTimes(1);
  391. expect(mockPrintTaskService.cancelPrintTask).toHaveBeenCalledWith(tenantId, 'TASK1', 'REFUND');
  392. expect(mockPrintTaskService.executePrintTask).not.toHaveBeenCalled();
  393. });
  394. });
  395. });