feat(dramas): 新增剧集搜索、推荐接口与管理筛选功能

1.  为后台剧集列表添加标题和推荐状态筛选参数
2.  新增前台推荐剧集列表接口和前置搜索接口
3.  添加可选布尔值解析工具方法
4.  新增对应接口的端到端测试用例
This commit is contained in:
2026-07-02 17:26:08 +08:00
parent f2b1043cbc
commit fca247c3f2
7 changed files with 1421 additions and 933 deletions

View File

@@ -212,6 +212,8 @@ describe('Dramas and episodes', () => {
.send({
title: 'Published Drama',
coverUrl: 'https://cdn.example.com/pub.jpg',
portraitCoverUrl: 'https://cdn.example.com/pub-portrait.jpg',
landscapeCoverUrl: 'https://cdn.example.com/pub-landscape.jpg',
type: 'action',
status: 'published',
})
@@ -265,6 +267,11 @@ describe('Dramas and episodes', () => {
expect.objectContaining({ title: 'Published Drama' }),
]),
);
const publishedDrama = dramas.body.data.list.find(
(item: { title: string }) => item.title === 'Published Drama',
);
expect(publishedDrama).not.toHaveProperty('portraitCoverUrl');
expect(publishedDrama).not.toHaveProperty('landscapeCoverUrl');
expect(dramas.body.data.list).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Draft Drama' })]),
);
@@ -321,4 +328,178 @@ describe('Dramas and episodes', () => {
]),
);
});
it('filters admin dramas by title and recommended status', async () => {
await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Recommended Search Drama',
coverUrl: 'https://cdn.example.com/recommended-search.jpg',
type: 'romance',
status: 'draft',
isRecommended: true,
})
.expect(201);
await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Recommended Hidden Drama',
coverUrl: 'https://cdn.example.com/recommended-hidden.jpg',
type: 'romance',
status: 'draft',
isRecommended: false,
})
.expect(201);
const response = await request(app.getHttpServer())
.get('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.query({ title: 'Recommended', isRecommended: 'true' })
.expect(200);
expect(response.body.data.list).toEqual(
expect.arrayContaining([
expect.objectContaining({
title: 'Recommended Search Drama',
isRecommended: true,
}),
]),
);
expect(response.body.data.list).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'Recommended Hidden Drama' }),
]),
);
});
it('returns a published recommended drama list to app APIs', async () => {
await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Published Recommended Drama',
coverUrl: 'https://cdn.example.com/published-recommended.jpg',
type: 'romance',
status: 'published',
isRecommended: true,
})
.expect(201);
await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Published Normal Drama',
coverUrl: 'https://cdn.example.com/published-normal.jpg',
type: 'romance',
status: 'published',
isRecommended: false,
})
.expect(201);
const response = await request(app.getHttpServer())
.get('/api/app/v1/dramas/recommended')
.expect(200);
expect(response.body.data.list).toEqual(
expect.arrayContaining([
expect.objectContaining({
title: 'Published Recommended Drama',
isRecommended: true,
}),
]),
);
expect(response.body.data.list).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'Published Normal Drama' }),
]),
);
});
it('presearches published approved videos by keyword for mini app clients', async () => {
const drama = await request(app.getHttpServer())
.post('/api/admin/v1/dramas')
.set('Authorization', `Bearer ${adminToken}`)
.send({
title: 'Skyline Keyword Saga',
description: 'A searchable drama description.',
coverUrl: 'https://cdn.example.com/skyline-drama.jpg',
type: 'romance',
status: 'published',
})
.expect(201);
await request(app.getHttpServer())
.post('/api/admin/v1/episodes')
.set('Authorization', `Bearer ${adminToken}`)
.send({
dramaId: drama.body.data.id,
episodeNo: 1,
title: 'Pilot Video Match',
description: 'The first skyline episode.',
coverUrl: 'https://cdn.example.com/skyline-ep1.jpg',
videoUrl: 'https://cdn.example.com/skyline-ep1.mp4',
byteplusVid: 'vid_skyline_001',
durationSeconds: 180,
freeType: 'free',
price: 0,
status: 'published',
publishStatus: 'published',
reviewStatus: 'approved',
})
.expect(201);
await request(app.getHttpServer())
.post('/api/admin/v1/episodes')
.set('Authorization', `Bearer ${adminToken}`)
.send({
dramaId: drama.body.data.id,
episodeNo: 2,
title: 'Hidden Draft Video',
description: 'Contains skyline but is not published.',
coverUrl: 'https://cdn.example.com/skyline-ep2.jpg',
videoUrl: 'https://cdn.example.com/skyline-ep2.mp4',
byteplusVid: 'vid_skyline_002',
durationSeconds: 180,
status: 'draft',
publishStatus: 'draft',
reviewStatus: 'approved',
})
.expect(201);
const response = await request(app.getHttpServer())
.get('/api/app/v1/dramas/presearch')
.query({ keyword: 'skyline' })
.expect(200);
expect(response.body.data.pagination.pageSize).toBe(10);
expect(response.body.data.list).toEqual([
expect.objectContaining({
dramaId: drama.body.data.id,
episodeNo: 1,
title: 'Pilot Video Match',
coverUrl: 'https://cdn.example.com/skyline-ep1.jpg',
videoUrl: 'https://cdn.example.com/skyline-ep1.mp4',
byteplusVid: 'vid_skyline_001',
durationSeconds: 180,
freeType: 'free',
price: 0,
dramaTitle: 'Skyline Keyword Saga',
dramaCoverUrl: 'https://cdn.example.com/skyline-drama.jpg',
}),
]);
const emptyKeyword = await request(app.getHttpServer())
.get('/api/app/v1/dramas/presearch')
.query({ keyword: ' ' })
.expect(200);
expect(emptyKeyword.body.data).toEqual({
list: [],
pagination: { page: 1, pageSize: 10, total: 0 },
});
});
});