forked from datawhale/whale-town-end
db:添加通知系统数据库支持
- 创建notices表结构SQL脚本 - 包含完整的字段定义和索引优化 - 添加通知服务单元测试用例
This commit is contained in:
21
src/business/notice/migrations/create-notices-table.sql
Normal file
21
src/business/notice/migrations/create-notices-table.sql
Normal file
@@ -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='通知表';
|
||||||
202
src/business/notice/notice.service.spec.ts
Normal file
202
src/business/notice/notice.service.spec.ts
Normal file
@@ -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<Notice>;
|
||||||
|
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>(NoticeService);
|
||||||
|
repository = module.get<Repository<Notice>>(getRepositoryToken(Notice));
|
||||||
|
gateway = module.get<NoticeGateway>(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 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user