feat: 添加频道管理功能,关联短剧与频道绑定逻辑

1. 新增频道模块,包含类型定义、实体、DTO、控制器和服务
2. 为短剧添加频道ID和频道名字段,支持关联频道
3. 新增短剧删除API和权限配置
4. 调整tsconfig和依赖配置,更新权限列表
5. 新增相关测试用例验证频道与短剧关联逻辑
This commit is contained in:
2026-07-03 15:31:36 +08:00
parent 4eff4d877e
commit 2eeec11d51
17 changed files with 800 additions and 40 deletions

View File

@@ -24,6 +24,210 @@ describe('Dramas and episodes', () => {
await app.close();
});
it('lists the default channel for admin selectors', async () => {
const response = await request(app.getHttpServer())
.get('/api/admin/v1/channels')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
expect(response.body.data.list).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
name: '默认频道',
code: 'default',
isDefault: true,
status: 'active',
}),
]),
);
});
it('assigns dramas to the default channel when no channel is selected', async () => {
const dramaResponse = await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Default Channel Drama',
coverUrl: 'https://cdn.example.com/default-channel.jpg',
type: 'romance',
status: 'draft',
})
.expect(201);
expect(dramaResponse.body.data).toMatchObject({
title: 'Default Channel Drama',
channelId: expect.any(Number),
channelName: '默认频道',
});
const detail = await request(app.getHttpServer())
.get(`/api/admin/v1/dramas/${dramaResponse.body.data.id}`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
expect(detail.body.data).toMatchObject({
id: dramaResponse.body.data.id,
channelId: dramaResponse.body.data.channelId,
channelName: '默认频道',
});
});
it('allows creating a drama without a type', async () => {
const response = await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'No Type Drama',
coverUrl: 'https://cdn.example.com/no-type.jpg',
status: 'draft',
})
.expect(201);
expect(response.body.data).toMatchObject({
title: 'No Type Drama',
type: '',
channelName: '默认频道',
});
});
it('deletes a drama and its episodes from admin APIs', async () => {
const drama = await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Delete Drama Test',
coverUrl: 'https://cdn.example.com/delete-drama.jpg',
type: 'action',
status: 'draft',
})
.expect(201);
await request(app.getHttpServer())
.post('/api/admin/v1/episodes')
.set('Authorization', `Bearer ${adminToken}`)
.send({
dramaId: drama.body.data.id,
episodeNo: 1,
title: 'Delete Episode',
coverUrl: 'https://cdn.example.com/delete-episode.jpg',
durationSeconds: 300,
status: 'draft',
})
.expect(201);
await request(app.getHttpServer())
.delete(`/api/admin/v1/dramas/${drama.body.data.id}`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
await request(app.getHttpServer())
.get(`/api/admin/v1/dramas/${drama.body.data.id}`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(404);
const list = await request(app.getHttpServer())
.get('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.query({ title: 'Delete Drama Test' })
.expect(200);
expect(list.body.data.list).toEqual([]);
});
it('supports assigning a drama to a selected channel and rejects missing channels', async () => {
const channelResponse = await request(app.getHttpServer())
.post('/api/admin/v1/channels')
.set('Authorization', `Bearer ${adminToken}`)
.send({
name: '测试频道',
code: 'test-channel',
sortOrder: 10,
})
.expect(201);
const dramaResponse = await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Selected Channel Drama',
coverUrl: 'https://cdn.example.com/selected-channel.jpg',
type: 'action',
status: 'draft',
channelId: channelResponse.body.data.id,
})
.expect(201);
expect(dramaResponse.body.data).toMatchObject({
title: 'Selected Channel Drama',
channelId: channelResponse.body.data.id,
channelName: '测试频道',
});
await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Missing Channel Drama',
coverUrl: 'https://cdn.example.com/missing-channel.jpg',
type: 'action',
status: 'draft',
channelId: 999999,
})
.expect(404);
});
it('updates and deletes non-default channels but keeps the default channel protected', async () => {
const created = await request(app.getHttpServer())
.post('/api/admin/v1/channels')
.set('Authorization', `Bearer ${adminToken}`)
.send({
name: '待编辑频道',
code: 'editable-channel',
sortOrder: 1,
})
.expect(201);
const updated = await request(app.getHttpServer())
.patch(`/api/admin/v1/channels/${created.body.data.id}`)
.set('Authorization', `Bearer ${adminToken}`)
.send({
name: '已编辑频道',
sortOrder: 2,
})
.expect(200);
expect(updated.body.data).toMatchObject({
id: created.body.data.id,
name: '已编辑频道',
sortOrder: 2,
});
await request(app.getHttpServer())
.delete(`/api/admin/v1/channels/${created.body.data.id}`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
const channels = await request(app.getHttpServer())
.get('/api/admin/v1/channels')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
expect(channels.body.data.list).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ code: 'editable-channel' }),
]),
);
const defaultChannel = channels.body.data.list.find(
(channel: { code: string }) => channel.code === 'default',
);
await request(app.getHttpServer())
.delete(`/api/admin/v1/channels/${defaultChannel.id}`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(422);
});
it('allows admin APIs to create a drama and episodes with release metadata', async () => {
const dramaResponse = await request(app.getHttpServer())
.post('/api/admin/v1/dramas')