feat(dramas): 新增剧集搜索、推荐接口与管理筛选功能
1. 为后台剧集列表添加标题和推荐状态筛选参数 2. 新增前台推荐剧集列表接口和前置搜索接口 3. 添加可选布尔值解析工具方法 4. 新增对应接口的端到端测试用例
This commit is contained in:
@@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,4 +128,164 @@ describe('Auth, orders, and playback', () => {
|
||||
videoUrl: 'https://cdn.example.com/paid-ep.mp4',
|
||||
});
|
||||
});
|
||||
|
||||
it('records, updates, and isolates mini app playback history', async () => {
|
||||
const firstLogin = await request(app.getHttpServer())
|
||||
.post('/api/app/v1/auth/tiktok-login')
|
||||
.send({
|
||||
code: 'history-user-code-1',
|
||||
mockOpenId: 'history-user-1',
|
||||
nickname: 'History User 1',
|
||||
})
|
||||
.expect(201);
|
||||
const firstToken = firstLogin.body.data.accessToken;
|
||||
|
||||
const secondLogin = await request(app.getHttpServer())
|
||||
.post('/api/app/v1/auth/tiktok-login')
|
||||
.send({
|
||||
code: 'history-user-code-2',
|
||||
mockOpenId: 'history-user-2',
|
||||
nickname: 'History User 2',
|
||||
})
|
||||
.expect(201);
|
||||
const secondToken = secondLogin.body.data.accessToken;
|
||||
|
||||
const drama = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/dramas')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
title: 'History Drama',
|
||||
coverUrl: 'https://cdn.example.com/history-drama.jpg',
|
||||
type: 'history',
|
||||
status: 'published',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const episode = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/episodes')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
dramaId: drama.body.data.id,
|
||||
episodeNo: 1,
|
||||
title: 'History Episode',
|
||||
coverUrl: 'https://cdn.example.com/history-episode.jpg',
|
||||
videoUrl: 'https://cdn.example.com/history-episode.mp4',
|
||||
durationSeconds: 300,
|
||||
status: 'published',
|
||||
publishStatus: 'published',
|
||||
reviewStatus: 'approved',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/app/v1/playback-history')
|
||||
.send({
|
||||
episodeId: episode.body.data.id,
|
||||
progressSeconds: 86,
|
||||
completed: false,
|
||||
occurredAt: '2026-07-02T10:00:00.000Z',
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/app/v1/playback-history')
|
||||
.set('Authorization', `Bearer ${firstToken}`)
|
||||
.send({
|
||||
episodeId: episode.body.data.id,
|
||||
progressSeconds: 86,
|
||||
completed: false,
|
||||
occurredAt: '2026-07-02T10:00:00.000Z',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/app/v1/playback-history')
|
||||
.set('Authorization', `Bearer ${firstToken}`)
|
||||
.send({
|
||||
episodeId: episode.body.data.id,
|
||||
progressSeconds: 301,
|
||||
completed: false,
|
||||
occurredAt: '2026-07-02T10:05:00.000Z',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const firstHistory = await request(app.getHttpServer())
|
||||
.get('/api/app/v1/playback-history')
|
||||
.set('Authorization', `Bearer ${firstToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(firstHistory.body.data).toMatchObject({
|
||||
pagination: { page: 1, pageSize: 20, total: 1 },
|
||||
list: [
|
||||
{
|
||||
dramaId: drama.body.data.id,
|
||||
episodeId: episode.body.data.id,
|
||||
episodeNo: 1,
|
||||
title: 'History Episode',
|
||||
coverUrl: 'https://cdn.example.com/history-episode.jpg',
|
||||
progressSeconds: 300,
|
||||
durationSeconds: 300,
|
||||
completed: true,
|
||||
lastPlayedAt: '2026-07-02T10:05:00.000Z',
|
||||
dramaTitle: 'History Drama',
|
||||
dramaCoverUrl: 'https://cdn.example.com/history-drama.jpg',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const secondHistory = await request(app.getHttpServer())
|
||||
.get('/api/app/v1/playback-history')
|
||||
.set('Authorization', `Bearer ${secondToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(secondHistory.body.data).toMatchObject({
|
||||
pagination: { page: 1, pageSize: 20, total: 0 },
|
||||
list: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects playback history for unavailable episodes', async () => {
|
||||
const login = await request(app.getHttpServer())
|
||||
.post('/api/app/v1/auth/tiktok-login')
|
||||
.send({
|
||||
code: 'history-user-code-3',
|
||||
mockOpenId: 'history-user-3',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const drama = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/dramas')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
title: 'Draft History Drama',
|
||||
coverUrl: 'https://cdn.example.com/draft-history-drama.jpg',
|
||||
type: 'history',
|
||||
status: 'published',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const episode = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/episodes')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
dramaId: drama.body.data.id,
|
||||
episodeNo: 1,
|
||||
title: 'Draft History Episode',
|
||||
coverUrl: 'https://cdn.example.com/draft-history-episode.jpg',
|
||||
durationSeconds: 120,
|
||||
status: 'draft',
|
||||
publishStatus: 'draft',
|
||||
reviewStatus: 'not_submitted',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/app/v1/playback-history')
|
||||
.set('Authorization', `Bearer ${login.body.data.accessToken}`)
|
||||
.send({
|
||||
episodeId: episode.body.data.id,
|
||||
progressSeconds: 10,
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user