import { describe, it, expect, beforeEach, vi } from 'vitest'; import { DataSource, Repository } from 'typeorm'; import { OrderMt } from '@d8d/orders-module-mt'; import { DataOverviewServiceMt, TimeFilterParams, SummaryStatistics } from '../../src/services/data-overview.service'; // Mock redisUtil and AppDataSource - use hoisted to ensure availability before vi.mock const { mockRedisUtil, mockAppDataSource } = vi.hoisted(() => { return { mockRedisUtil: { get: vi.fn(), set: vi.fn(), keys: vi.fn(), del: vi.fn() }, mockAppDataSource: {} }; }); vi.mock('@d8d/shared-utils', async () => { const actual = await vi.importActual('@d8d/shared-utils'); return { ...actual, redisUtil: mockRedisUtil, AppDataSource: mockAppDataSource }; }); describe('DataOverviewServiceMt', () => { let service: DataOverviewServiceMt; let mockDataSource: DataSource; let mockOrderRepository: Repository; beforeEach(() => { // Mock Order Repository mockOrderRepository = { createQueryBuilder: vi.fn(() => ({ select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), setParameters: vi.fn().mockReturnThis(), getRawOne: vi.fn() })) } as any; // Mock DataSource mockDataSource = { getRepository: vi.fn((entity) => { if (entity === OrderMt) { return mockOrderRepository; } return {} as any; }) } as any; // Reset redisUtil mocks mockRedisUtil.get.mockReset(); mockRedisUtil.set.mockReset(); mockRedisUtil.keys.mockReset(); mockRedisUtil.del.mockReset(); service = new DataOverviewServiceMt(mockDataSource); }); describe('getDateRange', () => { it('应该返回今天的时间范围(默认)', () => { const now = new Date('2025-12-26T10:00:00Z'); vi.setSystemTime(now); const params: TimeFilterParams = {}; const result = service['getDateRange'](params); const expectedStart = new Date('2025-12-26T00:00:00Z'); const expectedEnd = now; // 对于今天的时间范围,结束时间是当前时间 expect(result.startDate.toISOString()).toBe(expectedStart.toISOString()); expect(result.endDate.toISOString()).toBe(expectedEnd.toISOString()); }); it('应该返回昨天的时间范围', () => { const now = new Date('2025-12-26T10:00:00Z'); vi.setSystemTime(now); const params: TimeFilterParams = { timeRange: 'yesterday' }; const result = service['getDateRange'](params); const expectedStart = new Date('2025-12-25T00:00:00Z'); const expectedEnd = new Date('2025-12-25T23:59:59.999Z'); expect(result.startDate.toISOString()).toBe(expectedStart.toISOString()); expect(result.endDate.toISOString()).toBe(expectedEnd.toISOString()); }); it('应该返回最近7天的时间范围', () => { const now = new Date('2025-12-26T10:00:00Z'); vi.setSystemTime(now); const params: TimeFilterParams = { timeRange: 'last7days' }; const result = service['getDateRange'](params); const expectedStart = new Date('2025-12-19T10:00:00Z'); // 7天前 const expectedEnd = now; expect(result.startDate.toISOString()).toBe(expectedStart.toISOString()); expect(result.endDate.toISOString()).toBe(expectedEnd.toISOString()); }); it('应该返回最近30天的时间范围', () => { const now = new Date('2025-12-26T10:00:00Z'); vi.setSystemTime(now); const params: TimeFilterParams = { timeRange: 'last30days' }; const result = service['getDateRange'](params); const expectedStart = new Date('2025-11-26T10:00:00Z'); // 30天前 const expectedEnd = now; expect(result.startDate.toISOString()).toBe(expectedStart.toISOString()); expect(result.endDate.toISOString()).toBe(expectedEnd.toISOString()); }); it('应该返回自定义时间范围', () => { const params: TimeFilterParams = { timeRange: 'custom', startDate: '2025-01-01T00:00:00Z', endDate: '2025-01-31T23:59:59Z' }; const result = service['getDateRange'](params); expect(result.startDate.toISOString()).toBe('2025-01-01T00:00:00.000Z'); expect(result.endDate.toISOString()).toBe('2025-01-31T23:59:59.000Z'); }); it('当自定义时间范围缺少参数时应该使用默认值', () => { const now = new Date('2025-12-26T10:00:00Z'); vi.setSystemTime(now); const params: TimeFilterParams = { timeRange: 'custom' }; // 缺少startDate和endDate const result = service['getDateRange'](params); const expectedStart = new Date('2025-12-26T00:00:00Z'); const expectedEnd = now; // 当自定义范围缺少参数时,使用默认的今天范围,结束时间为当前时间 expect(result.startDate.toISOString()).toBe(expectedStart.toISOString()); expect(result.endDate.toISOString()).toBe(expectedEnd.toISOString()); }); }); describe('getSummaryStatistics', () => { it('应该从缓存返回统计数据', async () => { const tenantId = 1; const params: TimeFilterParams = { timeRange: 'today' }; const cacheKey = `data_overview:summary:${tenantId}:today::`; const cachedStats: SummaryStatistics = { totalSales: 10000, totalOrders: 50, wechatSales: 6000, wechatOrders: 30, creditSales: 4000, creditOrders: 20, todaySales: 500, todayOrders: 5 }; mockRedisUtil.get.mockResolvedValue(JSON.stringify(cachedStats)); const result = await service.getSummaryStatistics(tenantId, params); expect(result).toEqual(cachedStats); expect(mockRedisUtil.get).toHaveBeenCalledWith(cacheKey); expect(mockRedisUtil.set).not.toHaveBeenCalled(); }); it('当缓存未命中时应该查询数据库并设置缓存', async () => { const tenantId = 1; const params: TimeFilterParams = { timeRange: 'today' }; const cacheKey = `data_overview:summary:${tenantId}:today::`; mockRedisUtil.get.mockResolvedValue(null); // Mock database query result const mockQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), setParameters: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ total_orders: '50', total_sales: '10000.50', wechat_sales: '6000.00', credit_sales: '4000.50', wechat_orders: '30', credit_orders: '20' }) }; const mockTodayQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ today_orders: '5', today_sales: '500.00' }) }; vi.mocked(mockOrderRepository.createQueryBuilder) .mockReturnValueOnce(mockQueryBuilder as any) .mockReturnValueOnce(mockTodayQueryBuilder as any); const result = await service.getSummaryStatistics(tenantId, params); expect(result.totalSales).toBe(10000.50); expect(result.totalOrders).toBe(50); expect(result.wechatSales).toBe(6000); expect(result.creditSales).toBe(4000.50); expect(result.wechatOrders).toBe(30); expect(result.creditOrders).toBe(20); expect(result.todaySales).toBe(500); expect(result.todayOrders).toBe(5); expect(mockRedisUtil.set).toHaveBeenCalledWith( cacheKey, expect.any(String), 5 * 60 // 5分钟TTL(今日数据) ); }); it('应该为历史数据设置30分钟缓存', async () => { const tenantId = 1; const params: TimeFilterParams = { timeRange: 'last7days' }; const cacheKey = `data_overview:summary:${tenantId}:last7days::`; mockRedisUtil.get.mockResolvedValue(null); const mockQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), setParameters: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ total_orders: '100', total_sales: '20000.00', wechat_sales: '12000.00', credit_sales: '8000.00', wechat_orders: '60', credit_orders: '40' }) }; const mockTodayQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ today_orders: '10', today_sales: '1000.00' }) }; vi.mocked(mockOrderRepository.createQueryBuilder) .mockReturnValueOnce(mockQueryBuilder as any) .mockReturnValueOnce(mockTodayQueryBuilder as any); await service.getSummaryStatistics(tenantId, params); expect(mockRedisUtil.set).toHaveBeenCalledWith( cacheKey, expect.any(String), 30 * 60 // 30分钟TTL(历史数据) ); }); }); describe('getTodayStatistics', () => { it('应该从缓存返回今日统计数据', async () => { const tenantId = 1; const today = new Date().toISOString().split('T')[0]; const cacheKey = `data_overview:today:${tenantId}:${today}`; const cachedStats = { todaySales: 500, todayOrders: 5 }; mockRedisUtil.get.mockResolvedValue(JSON.stringify(cachedStats)); const result = await service.getTodayStatistics(tenantId); expect(result).toEqual(cachedStats); expect(mockRedisUtil.get).toHaveBeenCalledWith(cacheKey); expect(mockRedisUtil.set).not.toHaveBeenCalled(); }); it('当缓存未命中时应该查询数据库并设置缓存', async () => { const tenantId = 1; const today = new Date().toISOString().split('T')[0]; const cacheKey = `data_overview:today:${tenantId}:${today}`; mockRedisUtil.get.mockResolvedValue(null); const mockQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ today_orders: '5', today_sales: '500.00' }) }; vi.mocked(mockOrderRepository.createQueryBuilder).mockReturnValue(mockQueryBuilder as any); const result = await service.getTodayStatistics(tenantId); expect(result.todaySales).toBe(500); expect(result.todayOrders).toBe(5); expect(mockRedisUtil.set).toHaveBeenCalledWith(cacheKey, expect.any(String), 5 * 60); }); }); describe('clearCache', () => { it('应该清理指定租户的所有缓存', async () => { const tenantId = 1; const cacheKeys = [ 'data_overview:summary:1:today::', 'data_overview:today:1:2025-12-26' ]; mockRedisUtil.keys.mockResolvedValue(cacheKeys); await service.clearCache(tenantId); expect(mockRedisUtil.keys).toHaveBeenCalledWith('data_overview:*:1:*'); expect(mockRedisUtil.del).toHaveBeenCalledWith(...cacheKeys); }); it('当没有缓存键时不应该调用del', async () => { const tenantId = 1; mockRedisUtil.keys.mockResolvedValue([]); await service.clearCache(tenantId); expect(mockRedisUtil.keys).toHaveBeenCalledWith('data_overview:*:1:*'); expect(mockRedisUtil.del).not.toHaveBeenCalled(); }); }); describe('getUserConsumptionStatistics', () => { const tenantId = 1; const mockUsers = [ { userId: 1, userName: '张三', userPhone: '13800138001', totalSpent: 15000.50, orderCount: 15, avgOrderAmount: 1000.03, lastOrderDate: '2025-12-30T10:30:00Z' }, { userId: 2, userName: '李四', userPhone: '13800138002', totalSpent: 12000.75, orderCount: 12, avgOrderAmount: 1000.06, lastOrderDate: '2025-12-29T14:20:00Z' }, { userId: 3, userName: '王五', userPhone: '13800138003', totalSpent: 8000.25, orderCount: 8, avgOrderAmount: 1000.03, lastOrderDate: '2025-12-28T09:15:00Z' } ]; beforeEach(() => { vi.clearAllMocks(); }); it('应该从缓存返回用户消费统计数据', async () => { const params = { timeRange: 'last30days' } as any; const paginationParams = { page: 1, limit: 10, sortBy: 'totalSpent' as const, sortOrder: 'desc' as const }; const cacheKey = `data_overview:user_consumption:${tenantId}:last30days:::1:10:totalSpent:desc`; const cachedResponse = { items: mockUsers, pagination: { page: 1, limit: 10, total: 3, totalPages: 1 } }; mockRedisUtil.get.mockResolvedValue(JSON.stringify(cachedResponse)); const result = await service.getUserConsumptionStatistics(tenantId, params, paginationParams); expect(result).toEqual(cachedResponse); expect(mockRedisUtil.get).toHaveBeenCalledWith(cacheKey); expect(mockRedisUtil.set).not.toHaveBeenCalled(); }); it('当缓存未命中时应该查询数据库并设置缓存', async () => { const params = { timeRange: 'last30days' } as any; const paginationParams = { page: 1, limit: 10, sortBy: 'totalSpent' as const, sortOrder: 'desc' as const }; const cacheKey = `data_overview:user_consumption:${tenantId}:last30days:::1:10:totalSpent:desc`; mockRedisUtil.get.mockResolvedValue(null); // Mock count query builder const mockCountQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ total_users: '3' }) }; // Mock main query builder const mockQueryBuilder = { select: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), groupBy: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), offset: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), getRawMany: vi.fn().mockResolvedValue(mockUsers.map(user => ({ userId: user.userId, userName: user.userName, userPhone: user.userPhone, totalSpent: user.totalSpent.toString(), orderCount: user.orderCount.toString(), avgOrderAmount: user.avgOrderAmount.toString(), lastOrderDate: user.lastOrderDate }))) }; vi.mocked(mockOrderRepository.createQueryBuilder) .mockReturnValueOnce(mockCountQueryBuilder as any) .mockReturnValueOnce(mockQueryBuilder as any); const result = await service.getUserConsumptionStatistics(tenantId, params, paginationParams); expect(result.items).toHaveLength(3); expect(result.pagination.page).toBe(1); expect(result.pagination.limit).toBe(10); expect(result.pagination.total).toBe(3); expect(result.pagination.totalPages).toBe(1); expect(result.items[0].userId).toBe(1); expect(result.items[0].userName).toBe('张三'); expect(result.items[0].totalSpent).toBe(15000.50); expect(result.items[0].orderCount).toBe(15); expect(mockRedisUtil.set).toHaveBeenCalledWith(cacheKey, expect.any(String), 30 * 60); }); it('应该正确处理空结果', async () => { const params = { timeRange: 'today' } as any; const paginationParams = { page: 1, limit: 10 }; mockRedisUtil.get.mockResolvedValue(null); // Mock count query builder with zero results const mockCountQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ total_users: '0' }) }; vi.mocked(mockOrderRepository.createQueryBuilder) .mockReturnValueOnce(mockCountQueryBuilder as any); const result = await service.getUserConsumptionStatistics(tenantId, params, paginationParams); expect(result.items).toHaveLength(0); expect(result.pagination.total).toBe(0); expect(result.pagination.totalPages).toBe(0); }); it('应该支持不同的排序字段和方向', async () => { const params = { timeRange: 'last30days' } as any; const paginationParams = { page: 1, limit: 10, sortBy: 'orderCount' as const, sortOrder: 'asc' as const }; mockRedisUtil.get.mockResolvedValue(null); // Mock count query builder const mockCountQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ total_users: '2' }) }; // Mock main query builder const mockQueryBuilder = { select: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), groupBy: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), offset: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), getRawMany: vi.fn().mockResolvedValue(mockUsers.slice(0, 2).map(user => ({ userId: user.userId, userName: user.userName, userPhone: user.userPhone, totalSpent: user.totalSpent.toString(), orderCount: user.orderCount.toString(), avgOrderAmount: user.avgOrderAmount.toString(), lastOrderDate: user.lastOrderDate }))) }; vi.mocked(mockOrderRepository.createQueryBuilder) .mockReturnValueOnce(mockCountQueryBuilder as any) .mockReturnValueOnce(mockQueryBuilder as any); await service.getUserConsumptionStatistics(tenantId, params, paginationParams); // 验证orderBy被正确调用 expect(mockQueryBuilder.orderBy).toHaveBeenCalled(); }); it('应该支持分页', async () => { const params = { timeRange: 'last30days' } as any; const paginationParams = { page: 2, limit: 5 }; mockRedisUtil.get.mockResolvedValue(null); // Mock count query builder const mockCountQueryBuilder = { select: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), getRawOne: vi.fn().mockResolvedValue({ total_users: '15' }) }; // Mock main query builder const mockQueryBuilder = { select: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), groupBy: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), offset: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), getRawMany: vi.fn().mockResolvedValue([]) }; vi.mocked(mockOrderRepository.createQueryBuilder) .mockReturnValueOnce(mockCountQueryBuilder as any) .mockReturnValueOnce(mockQueryBuilder as any); await service.getUserConsumptionStatistics(tenantId, params, paginationParams); // 验证offset被正确调用(第2页,每页5条 => offset = (2-1)*5 = 5) expect(mockQueryBuilder.offset).toHaveBeenCalledWith(5); expect(mockQueryBuilder.limit).toHaveBeenCalledWith(5); }); }); });