|
|
@@ -0,0 +1,537 @@
|
|
|
+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<typeof import('@d8d/shared-utils')>('@d8d/shared-utils');
|
|
|
+ return {
|
|
|
+ ...actual,
|
|
|
+ redisUtil: mockRedisUtil,
|
|
|
+ AppDataSource: mockAppDataSource
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+describe('DataOverviewServiceMt', () => {
|
|
|
+ let service: DataOverviewServiceMt;
|
|
|
+ let mockDataSource: DataSource;
|
|
|
+ let mockOrderRepository: Repository<OrderMt>;
|
|
|
+
|
|
|
+ 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);
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|