From 874ccfa879799279d4b776295db70e12c608cfcb Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Sat, 10 Jan 2026 21:52:48 +0800 Subject: [PATCH] =?UTF-8?q?db=EF=BC=9A=E6=B7=BB=E5=8A=A0=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=95=B0=E6=8D=AE=E5=BA=93=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建notices表结构SQL脚本 - 包含完整的字段定义和索引优化 - 添加通知服务单元测试用例 --- .../migrations/create-notices-table.sql | 21 ++ src/business/notice/notice.service.spec.ts | 202 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/business/notice/migrations/create-notices-table.sql create mode 100644 src/business/notice/notice.service.spec.ts diff --git a/src/business/notice/migrations/create-notices-table.sql b/src/business/notice/migrations/create-notices-table.sql new file mode 100644 index 0000000..a721a8d --- /dev/null +++ b/src/business/notice/migrations/create-notices-table.sql @@ -0,0 +1,21 @@ +-- 创建通知表 +CREATE TABLE IF NOT EXISTS `notices` ( + `id` int NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL COMMENT '通知标题', + `content` text NOT NULL COMMENT '通知内容', + `type` enum('system','user','broadcast') NOT NULL DEFAULT 'system' COMMENT '通知类型', + `status` enum('pending','sent','read','failed') NOT NULL DEFAULT 'pending' COMMENT '通知状态', + `userId` int DEFAULT NULL COMMENT '接收者用户ID,NULL表示广播', + `senderId` int DEFAULT NULL COMMENT '发送者用户ID', + `scheduledAt` datetime DEFAULT NULL COMMENT '计划发送时间', + `sentAt` datetime DEFAULT NULL COMMENT '实际发送时间', + `readAt` datetime DEFAULT NULL COMMENT '阅读时间', + `metadata` json DEFAULT NULL COMMENT '额外数据', + `createdAt` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', + `updatedAt` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_notices_user_id` (`userId`), + KEY `idx_notices_status` (`status`), + KEY `idx_notices_scheduled_at` (`scheduledAt`), + KEY `idx_notices_created_at` (`createdAt`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知表'; \ No newline at end of file diff --git a/src/business/notice/notice.service.spec.ts b/src/business/notice/notice.service.spec.ts new file mode 100644 index 0000000..fc2cba5 --- /dev/null +++ b/src/business/notice/notice.service.spec.ts @@ -0,0 +1,202 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NoticeService } from './notice.service'; +import { NoticeGateway } from './notice.gateway'; +import { Notice, NoticeStatus, NoticeType } from './notice.entity'; + +describe('NoticeService', () => { + let service: NoticeService; + let repository: Repository; + let gateway: NoticeGateway; + + const mockRepository = { + create: jest.fn(), + save: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), + count: jest.fn(), + createQueryBuilder: jest.fn(), + }; + + const mockGateway = { + sendToUser: jest.fn(), + broadcast: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeService, + { + provide: getRepositoryToken(Notice), + useValue: mockRepository, + }, + { + provide: NoticeGateway, + useValue: mockGateway, + }, + ], + }).compile(); + + service = module.get(NoticeService); + repository = module.get>(getRepositoryToken(Notice)); + gateway = module.get(NoticeGateway); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should create and send notice immediately when no scheduledAt', async () => { + const createDto = { + title: 'Test Notice', + content: 'Test Content', + userId: 1, + }; + + const mockNotice = { + id: 1, + ...createDto, + status: NoticeStatus.PENDING, + type: NoticeType.SYSTEM, + scheduledAt: null, + }; + + mockRepository.create.mockReturnValue(mockNotice); + mockRepository.save.mockResolvedValueOnce(mockNotice); + mockRepository.save.mockResolvedValueOnce({ + ...mockNotice, + status: NoticeStatus.SENT, + sentAt: new Date(), + }); + + const result = await service.create(createDto); + + expect(mockRepository.create).toHaveBeenCalledWith({ + ...createDto, + scheduledAt: null, + }); + expect(mockRepository.save).toHaveBeenCalledTimes(2); + expect(mockGateway.sendToUser).toHaveBeenCalledWith(1, { + type: 'notice', + data: mockNotice, + }); + }); + + it('should create scheduled notice without sending immediately', async () => { + const scheduledAt = new Date(Date.now() + 3600000); // 1 hour later + const createDto = { + title: 'Scheduled Notice', + content: 'Scheduled Content', + scheduledAt: scheduledAt.toISOString(), + }; + + const mockNotice = { + id: 1, + ...createDto, + scheduledAt, + status: NoticeStatus.PENDING, + }; + + mockRepository.create.mockReturnValue(mockNotice); + mockRepository.save.mockResolvedValue(mockNotice); + + const result = await service.create(createDto); + + expect(mockGateway.sendToUser).not.toHaveBeenCalled(); + expect(mockGateway.broadcast).not.toHaveBeenCalled(); + }); + }); + + describe('sendSystemNotice', () => { + it('should create and send system notice', async () => { + const mockNotice = { + id: 1, + title: 'System Notice', + content: 'System Content', + type: NoticeType.SYSTEM, + userId: 1, + status: NoticeStatus.PENDING, + }; + + mockRepository.create.mockReturnValue(mockNotice); + mockRepository.save.mockResolvedValueOnce(mockNotice); + mockRepository.save.mockResolvedValueOnce({ + ...mockNotice, + status: NoticeStatus.SENT, + }); + + const result = await service.sendSystemNotice('System Notice', 'System Content', 1); + + expect(result.type).toBe(NoticeType.SYSTEM); + expect(mockGateway.sendToUser).toHaveBeenCalled(); + }); + }); + + describe('sendBroadcast', () => { + it('should create and send broadcast notice', async () => { + const mockNotice = { + id: 1, + title: 'Broadcast Notice', + content: 'Broadcast Content', + type: NoticeType.BROADCAST, + userId: null, + status: NoticeStatus.PENDING, + }; + + mockRepository.create.mockReturnValue(mockNotice); + mockRepository.save.mockResolvedValueOnce(mockNotice); + mockRepository.save.mockResolvedValueOnce({ + ...mockNotice, + status: NoticeStatus.SENT, + }); + + const result = await service.sendBroadcast('Broadcast Notice', 'Broadcast Content'); + + expect(result.type).toBe(NoticeType.BROADCAST); + expect(mockGateway.broadcast).toHaveBeenCalled(); + }); + }); + + describe('markAsRead', () => { + it('should mark notice as read', async () => { + const mockNotice = { + id: 1, + userId: 1, + status: NoticeStatus.SENT, + }; + + const updatedNotice = { + ...mockNotice, + status: NoticeStatus.READ, + readAt: new Date(), + }; + + mockRepository.findOne.mockResolvedValue(mockNotice); + mockRepository.save.mockResolvedValue(updatedNotice); + + const result = await service.markAsRead(1, 1); + + expect(result.status).toBe(NoticeStatus.READ); + expect(result.readAt).toBeDefined(); + }); + }); + + describe('getUserUnreadCount', () => { + it('should return unread count for user', async () => { + mockRepository.count.mockResolvedValue(5); + + const count = await service.getUserUnreadCount(1); + + expect(count).toBe(5); + expect(mockRepository.count).toHaveBeenCalledWith({ + where: [ + { userId: 1, status: NoticeStatus.SENT }, + { userId: null, status: NoticeStatus.SENT }, + ], + }); + }); + }); +}); \ No newline at end of file