Files
cyh-tk-backend/src/modules/dramas/dramas.service.ts
zhxiao1124 f342312b92 refactor: 重构项目支持TypeORM数据库持久化
新增数据库实体类与服务实现,将内存存储扩展为支持数据库的完整实现,包括:
1. 新增Drama和Episode实体类,添加多个业务字段
2. 重构DramasService实现数据库CRUD与业务逻辑
3. 升级相关服务类为异步方法适配数据库操作
4. 更新测试用例适配异步接口变更
5. 完善分页、数据转换等辅助工具方法
2026-07-01 21:49:36 +08:00

939 lines
30 KiB
TypeScript

import {
ConflictException,
Injectable,
NotFoundException,
Optional,
UnprocessableEntityException,
} from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
import { ConfigureEpisodesDto } from './dto/configure-episodes.dto';
import { CreateDramaDto } from './dto/create-drama.dto';
import { CreateEpisodeDto } from './dto/create-episode.dto';
import { UpdateDramaDto } from './dto/update-drama.dto';
import { UpdateEpisodeDto } from './dto/update-episode.dto';
import { Drama } from './entities/drama.entity';
import { Episode } from './entities/episode.entity';
import { PublishStatus } from './drama-status.enum';
import {
DramaRecord,
EpisodeRecord,
} from './interfaces/drama-records.interface';
interface PaginationInput {
page: number;
pageSize: number;
}
@Injectable()
export class DramasService {
private dramaSequence = 1;
private episodeSequence = 1;
private readonly dramas: DramaRecord[] = [];
private readonly episodes: EpisodeRecord[] = [];
constructor(
@Optional()
@InjectDataSource()
readonly dataSource?: DataSource,
) {}
async createDrama(dto: CreateDramaDto): Promise<DramaRecord> {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(Drama);
const entity = repository.create({
tiktokAlbumId: dto.tiktokAlbumId,
title: dto.title,
description: dto.description,
coverId: dto.coverId,
coverUrl: dto.coverUrl,
portraitCoverUrl: dto.portraitCoverUrl,
landscapeCoverUrl: dto.landscapeCoverUrl,
type: dto.type,
tags: dto.tags ?? [],
region: dto.region,
language: dto.language,
year: dto.year,
status: dto.status ?? PublishStatus.Draft,
sortOrder: dto.sortOrder ?? 0,
isRecommended: dto.isRecommended ?? false,
onlineVersion: dto.onlineVersion ?? 1,
});
const saved = await repository.save(entity);
return this.toDramaRecord(saved, 0, 0);
}
const now = new Date().toISOString();
const drama: DramaRecord = {
id: this.dramaSequence++,
tiktokAlbumId: dto.tiktokAlbumId,
title: dto.title,
description: dto.description,
coverId: dto.coverId,
coverUrl: dto.coverUrl,
portraitCoverUrl: dto.portraitCoverUrl,
landscapeCoverUrl: dto.landscapeCoverUrl,
type: dto.type,
tags: dto.tags ?? [],
region: dto.region,
language: dto.language,
year: dto.year,
status: dto.status ?? PublishStatus.Draft,
sortOrder: dto.sortOrder ?? 0,
isRecommended: dto.isRecommended ?? false,
onlineVersion: dto.onlineVersion ?? 1,
totalEpisodes: 0,
publishedEpisodes: 0,
createdAt: now,
updatedAt: now,
};
this.dramas.push(drama);
return drama;
}
async listAdminDramas(
pagination: PaginationInput,
): Promise<PaginatedResponse<DramaRecord>> {
if (this.hasDatabase()) {
const { page, pageSize, skip, take } = this.resolvePagination(pagination);
const [records, total] = await this.dataSource
.getRepository(Drama)
.findAndCount({
order: { sortOrder: 'DESC', id: 'DESC' },
skip,
take,
});
const list = await Promise.all(
records.map((drama) => this.toDramaRecordWithCounts(drama)),
);
return { list, pagination: { page, pageSize, total } };
}
return this.paginate(
this.sortDramas(this.dramas.map((drama) => this.withEpisodeCounts(drama))),
pagination,
);
}
async getDramaOrThrow(id: number): Promise<DramaRecord> {
if (this.hasDatabase()) {
const drama = await this.dataSource
.getRepository(Drama)
.findOne({ where: { id } });
if (!drama) {
throw new NotFoundException('Drama not found');
}
return this.toDramaRecordWithCounts(drama);
}
const drama = this.dramas.find((item) => item.id === id);
if (!drama) {
throw new NotFoundException('Drama not found');
}
return this.withEpisodeCounts(drama);
}
async updateDrama(id: number, dto: UpdateDramaDto): Promise<DramaRecord> {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(Drama);
const drama = await repository.findOne({ where: { id } });
if (!drama) {
throw new NotFoundException('Drama not found');
}
repository.merge(drama, { ...dto, tags: dto.tags ?? drama.tags });
const saved = await repository.save(drama);
return this.toDramaRecordWithCounts(saved);
}
const drama = this.dramas.find((item) => item.id === id);
if (!drama) {
throw new NotFoundException('Drama not found');
}
Object.assign(drama, {
...dto,
tags: dto.tags ?? drama.tags,
updatedAt: new Date().toISOString(),
});
return this.withEpisodeCounts(drama);
}
async createEpisode(dto: CreateEpisodeDto): Promise<EpisodeRecord> {
const dramaId = dto.dramaId ?? dto.albumId;
const episodeNo = dto.episodeNo ?? dto.seq;
if (!dramaId || !episodeNo) {
throw new NotFoundException('Drama not found');
}
const freeType = dto.freeType ?? (dto.isPaid ? 'paid' : 'free');
const price = dto.price ?? dto.unlockPrice ?? 0;
const publishStatus = dto.publishStatus ?? dto.status ?? PublishStatus.Draft;
const reviewStatus =
dto.reviewStatus ??
(publishStatus === PublishStatus.Published ? 'approved' : 'not_submitted');
if (this.hasDatabase()) {
const dramaRepository = this.dataSource.getRepository(Drama);
const drama = await dramaRepository.findOne({ where: { id: dramaId } });
if (!drama) {
throw new NotFoundException('Drama not found');
}
const episodeRepository = this.dataSource.getRepository(Episode);
const duplicate = await episodeRepository.findOne({
where: { dramaId, episodeNo },
});
if (duplicate) {
throw new ConflictException(
'Episode number already exists in this drama',
);
}
const entity = episodeRepository.create({
dramaId,
tiktokEpisodeId: dto.tiktokEpisodeId,
episodeNo,
title: dto.title,
description: dto.description,
byteplusVid: dto.byteplusVid,
coverId: dto.coverId,
coverUrl: dto.coverUrl ?? '',
videoUrl: dto.videoUrl,
trialDurationSeconds: dto.trialDurationSeconds,
durationSeconds: dto.durationSeconds ?? 0,
width: dto.width,
height: dto.height,
isPaid: freeType === 'paid',
freeType,
unlockPrice: price,
price,
publishAt: dto.publishAt ? new Date(dto.publishAt) : undefined,
nextReleaseAt: dto.nextReleaseAt ? new Date(dto.nextReleaseAt) : undefined,
status: publishStatus,
reviewStatus,
publishStatus,
});
const saved = await episodeRepository.save(entity);
return this.toEpisodeRecord(saved);
}
const drama = this.dramas.find((item) => item.id === dramaId);
if (!drama) {
throw new NotFoundException('Drama not found');
}
const duplicate = this.episodes.some(
(item) => item.dramaId === dramaId && item.episodeNo === episodeNo,
);
if (duplicate) {
throw new ConflictException('Episode number already exists in this drama');
}
const now = new Date().toISOString();
const episode: EpisodeRecord = {
id: this.episodeSequence++,
dramaId,
albumId: dramaId,
tiktokEpisodeId: dto.tiktokEpisodeId,
episodeNo,
seq: episodeNo,
title: dto.title,
description: dto.description,
byteplusVid: dto.byteplusVid,
coverId: dto.coverId,
coverUrl: dto.coverUrl ?? '',
videoUrl: dto.videoUrl,
trialDurationSeconds: dto.trialDurationSeconds,
durationSeconds: dto.durationSeconds ?? 0,
width: dto.width,
height: dto.height,
isPaid: freeType === 'paid',
freeType,
unlockPrice: price,
price,
publishAt: dto.publishAt,
nextReleaseAt: dto.nextReleaseAt,
status: publishStatus,
reviewStatus,
publishStatus,
createdAt: now,
updatedAt: now,
};
this.episodes.push(episode);
return episode;
}
async configureEpisodes(
dramaId: number,
dto: ConfigureEpisodesDto,
): Promise<PaginatedResponse<EpisodeRecord>> {
if (this.hasDatabase()) {
const dramaRepository = this.dataSource.getRepository(Drama);
const drama = await dramaRepository.findOne({ where: { id: dramaId } });
if (!drama) {
throw new NotFoundException('Drama not found');
}
const episodeRepository = this.dataSource.getRepository(Episode);
const existing = await episodeRepository.find({
where: { dramaId },
order: { episodeNo: 'ASC' },
});
if (dto.episodeCount < existing.length) {
const removable = existing.slice(dto.episodeCount);
const hasLockedEpisode = removable.some(
(episode) =>
episode.byteplusVid ||
episode.publishStatus !== PublishStatus.Draft ||
episode.reviewStatus !== 'not_submitted',
);
if (hasLockedEpisode) {
throw new UnprocessableEntityException(
'Only empty draft tail episodes can be removed',
);
}
await episodeRepository.remove(removable);
}
for (
let episodeNo = existing.length + 1;
episodeNo <= dto.episodeCount;
episodeNo++
) {
await this.createEpisode({
dramaId,
episodeNo,
title: `${episodeNo}`,
coverUrl: drama.coverUrl,
durationSeconds: 0,
freeType: 'free',
price: 0,
reviewStatus: 'not_submitted',
publishStatus: PublishStatus.Draft,
status: PublishStatus.Draft,
});
}
const list = await episodeRepository.find({
where: { dramaId },
order: { episodeNo: 'ASC' },
});
const records = list.map((episode) => this.toEpisodeRecord(episode));
return {
list: records,
pagination: { page: 1, pageSize: records.length, total: records.length },
};
}
const drama = this.dramas.find((item) => item.id === dramaId);
if (!drama) {
throw new NotFoundException('Drama not found');
}
const existing = this.episodes
.filter((episode) => episode.dramaId === dramaId)
.sort((left, right) => left.episodeNo - right.episodeNo);
if (dto.episodeCount < existing.length) {
const removable = existing.slice(dto.episodeCount);
const hasLockedEpisode = removable.some(
(episode) =>
episode.byteplusVid ||
episode.publishStatus !== PublishStatus.Draft ||
episode.reviewStatus !== 'not_submitted',
);
if (hasLockedEpisode) {
throw new UnprocessableEntityException(
'Only empty draft tail episodes can be removed',
);
}
for (const episode of removable) {
const index = this.episodes.findIndex((item) => item.id === episode.id);
if (index >= 0) {
this.episodes.splice(index, 1);
}
}
}
for (
let episodeNo = existing.length + 1;
episodeNo <= dto.episodeCount;
episodeNo++
) {
await this.createEpisode({
dramaId,
episodeNo,
title: `${episodeNo}`,
coverUrl: drama.coverUrl,
durationSeconds: 0,
freeType: 'free',
price: 0,
reviewStatus: 'not_submitted',
publishStatus: PublishStatus.Draft,
status: PublishStatus.Draft,
});
}
const list = this.episodes
.filter((episode) => episode.dramaId === dramaId)
.sort((left, right) => left.episodeNo - right.episodeNo);
return {
list,
pagination: { page: 1, pageSize: list.length, total: list.length },
};
}
async listAdminEpisodes(
dramaId: number,
pagination: PaginationInput,
): Promise<PaginatedResponse<EpisodeRecord>> {
await this.getDramaOrThrow(dramaId);
if (this.hasDatabase()) {
const { page, pageSize, skip, take } = this.resolvePagination(pagination);
const [records, total] = await this.dataSource
.getRepository(Episode)
.findAndCount({
where: { dramaId },
order: { episodeNo: 'ASC' },
skip,
take,
});
return {
list: records.map((episode) => this.toEpisodeRecord(episode)),
pagination: { page, pageSize, total },
};
}
const records = this.episodes
.filter((episode) => episode.dramaId === dramaId)
.sort((left, right) => left.episodeNo - right.episodeNo);
return this.paginate(records, pagination);
}
async updateEpisode(id: number, dto: UpdateEpisodeDto): Promise<EpisodeRecord> {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(Episode);
const episode = await repository.findOne({ where: { id } });
if (!episode) {
throw new NotFoundException('Episode not found');
}
const freeType =
dto.freeType ??
(dto.isPaid === undefined
? episode.freeType
: dto.isPaid
? 'paid'
: 'free');
repository.merge(episode, {
...dto,
dramaId: dto.dramaId ?? dto.albumId ?? episode.dramaId,
episodeNo: dto.episodeNo ?? dto.seq ?? episode.episodeNo,
freeType,
isPaid: freeType === 'paid',
price: dto.price ?? dto.unlockPrice ?? episode.price,
unlockPrice: dto.unlockPrice ?? dto.price ?? episode.unlockPrice,
status: dto.status ?? dto.publishStatus ?? episode.status,
publishStatus: dto.publishStatus ?? dto.status ?? episode.publishStatus,
publishAt: dto.publishAt ? new Date(dto.publishAt) : episode.publishAt,
nextReleaseAt: dto.nextReleaseAt
? new Date(dto.nextReleaseAt)
: episode.nextReleaseAt,
});
const saved = await repository.save(episode);
return this.toEpisodeRecord(saved);
}
const episode = await this.getMemEpisodeOrThrow(id);
const freeType =
dto.freeType ??
(dto.isPaid === undefined
? episode.freeType
: dto.isPaid
? 'paid'
: 'free');
Object.assign(episode, {
...dto,
dramaId: dto.dramaId ?? dto.albumId ?? episode.dramaId,
albumId: dto.albumId ?? dto.dramaId ?? episode.albumId,
episodeNo: dto.episodeNo ?? dto.seq ?? episode.episodeNo,
seq: dto.seq ?? dto.episodeNo ?? episode.seq,
freeType,
isPaid: freeType === 'paid',
price: dto.price ?? dto.unlockPrice ?? episode.price,
unlockPrice: dto.unlockPrice ?? dto.price ?? episode.unlockPrice,
status: dto.status ?? dto.publishStatus ?? episode.status,
publishStatus: dto.publishStatus ?? dto.status ?? episode.publishStatus,
updatedAt: new Date().toISOString(),
});
return episode;
}
async attachEpisodeMedia(input: {
episodeId: number;
byteplusVid: string;
videoUrl: string;
storageBucket: string;
objectKey: string;
durationSeconds: number;
width: number;
height: number;
reviewStatus: EpisodeRecord['reviewStatus'];
reviewMessage?: string;
}): Promise<EpisodeRecord> {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(Episode);
const episode = await repository.findOne({
where: { id: input.episodeId },
});
if (!episode) {
throw new NotFoundException('Episode not found');
}
episode.byteplusVid = input.byteplusVid;
episode.videoUrl = input.videoUrl;
episode.storageBucket = input.storageBucket;
episode.objectKey = input.objectKey;
episode.durationSeconds = input.durationSeconds;
episode.width = input.width;
episode.height = input.height;
episode.reviewStatus = input.reviewStatus;
episode.reviewMessage = input.reviewMessage;
const saved = await repository.save(episode);
return this.toEpisodeRecord(saved);
}
const episode = await this.getMemEpisodeOrThrow(input.episodeId);
episode.byteplusVid = input.byteplusVid;
episode.videoUrl = input.videoUrl;
episode.storageBucket = input.storageBucket;
episode.objectKey = input.objectKey;
episode.durationSeconds = input.durationSeconds;
episode.width = input.width;
episode.height = input.height;
episode.reviewStatus = input.reviewStatus;
episode.reviewMessage = input.reviewMessage;
episode.updatedAt = new Date().toISOString();
return episode;
}
async updateEpisodeReviewStatus(
episodeId: number,
reviewStatus: EpisodeRecord['reviewStatus'],
reviewMessage?: string,
): Promise<EpisodeRecord> {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(Episode);
const episode = await repository.findOne({ where: { id: episodeId } });
if (!episode) {
throw new NotFoundException('Episode not found');
}
episode.reviewStatus = reviewStatus;
episode.reviewMessage = reviewMessage;
const saved = await repository.save(episode);
return this.toEpisodeRecord(saved);
}
const episode = await this.getMemEpisodeOrThrow(episodeId);
episode.reviewStatus = reviewStatus;
episode.reviewMessage = reviewMessage;
episode.updatedAt = new Date().toISOString();
return episode;
}
async listPublishedDramas(
pagination: PaginationInput,
): Promise<PaginatedResponse<DramaRecord>> {
if (this.hasDatabase()) {
const { page, pageSize, skip, take } = this.resolvePagination(pagination);
const [records, total] = await this.dataSource
.getRepository(Drama)
.findAndCount({
where: { status: PublishStatus.Published },
order: { sortOrder: 'DESC', id: 'DESC' },
skip,
take,
});
const list = await Promise.all(
records.map((drama) => this.toDramaRecordWithCounts(drama)),
);
return { list, pagination: { page, pageSize, total } };
}
return this.paginate(
this.sortDramas(
this.dramas
.filter((item) => item.status === PublishStatus.Published)
.map((drama) => this.withEpisodeCounts(drama)),
),
pagination,
);
}
async getPublishedDramaOrThrow(id: number): Promise<DramaRecord> {
if (this.hasDatabase()) {
const drama = await this.dataSource
.getRepository(Drama)
.findOne({ where: { id, status: PublishStatus.Published } });
if (!drama) {
throw new NotFoundException('Drama not found');
}
return this.toDramaRecordWithCounts(drama);
}
const drama = this.dramas.find(
(item) => item.id === id && item.status === PublishStatus.Published,
);
if (!drama) {
throw new NotFoundException('Drama not found');
}
return this.withEpisodeCounts(drama);
}
async listPublishedEpisodes(
dramaId: number,
pagination: PaginationInput,
): Promise<PaginatedResponse<EpisodeRecord>> {
if (this.hasDatabase()) {
const drama = await this.dataSource
.getRepository(Drama)
.findOne({ where: { id: dramaId, status: PublishStatus.Published } });
if (!drama) {
throw new NotFoundException('Drama not found');
}
const { page, pageSize, skip, take } = this.resolvePagination(pagination);
const [records, total] = await this.dataSource
.getRepository(Episode)
.findAndCount({
where: {
dramaId,
status: PublishStatus.Published,
publishStatus: PublishStatus.Published,
reviewStatus: 'approved',
},
order: { episodeNo: 'ASC' },
skip,
take,
});
return {
list: records.map((episode) => this.toEpisodeRecord(episode)),
pagination: { page, pageSize, total },
};
}
const drama = this.dramas.find((item) => item.id === dramaId);
if (!drama || drama.status !== PublishStatus.Published) {
throw new NotFoundException('Drama not found');
}
const episodes = this.episodes
.filter(
(item) =>
item.dramaId === dramaId && item.status === PublishStatus.Published,
)
.filter(
(item) =>
item.publishStatus === PublishStatus.Published &&
item.reviewStatus === 'approved',
)
.sort((left, right) => left.episodeNo - right.episodeNo);
return this.paginate(episodes, pagination);
}
async getPublishedEpisodeOrThrow(episodeId: number): Promise<EpisodeRecord> {
if (this.hasDatabase()) {
const episode = await this.dataSource.getRepository(Episode).findOne({
where: {
id: episodeId,
status: PublishStatus.Published,
publishStatus: PublishStatus.Published,
reviewStatus: 'approved',
},
});
if (!episode) {
throw new NotFoundException('Episode not found');
}
const drama = await this.dataSource
.getRepository(Drama)
.findOne({
where: { id: episode.dramaId, status: PublishStatus.Published },
});
if (!drama) {
throw new NotFoundException('Episode not found');
}
return this.toEpisodeRecord(episode);
}
const episode = this.episodes.find((item) => item.id === episodeId);
if (
!episode ||
episode.status !== PublishStatus.Published ||
episode.publishStatus !== PublishStatus.Published ||
episode.reviewStatus !== 'approved'
) {
throw new NotFoundException('Episode not found');
}
const drama = this.dramas.find((item) => item.id === episode.dramaId);
if (!drama || drama.status !== PublishStatus.Published) {
throw new NotFoundException('Episode not found');
}
return episode;
}
async getEpisodeOrThrow(episodeId: number): Promise<EpisodeRecord> {
if (this.hasDatabase()) {
const episode = await this.dataSource
.getRepository(Episode)
.findOne({ where: { id: episodeId } });
if (!episode) {
throw new NotFoundException('Episode not found');
}
return this.toEpisodeRecord(episode);
}
return this.getMemEpisodeOrThrow(episodeId);
}
async submitReview(albumId: number) {
if (this.hasDatabase()) {
const dramaRepository = this.dataSource.getRepository(Drama);
const drama = await dramaRepository.findOne({ where: { id: albumId } });
if (!drama) {
throw new NotFoundException('Drama not found');
}
const episodeRepository = this.dataSource.getRepository(Episode);
const episodes = await episodeRepository.find({
where: { dramaId: albumId },
});
for (const episode of episodes) {
episode.reviewStatus = 'approved';
episode.reviewMessage = 'Mock review approved';
}
await episodeRepository.save(episodes);
return { albumId, reviewStatus: 'approved' };
}
const album = this.dramas.find((item) => item.id === albumId);
if (!album) {
throw new NotFoundException('Drama not found');
}
this.episodes
.filter((episode) => episode.albumId === albumId)
.forEach((episode) => {
episode.reviewStatus = 'approved';
episode.reviewMessage = 'Mock review approved';
episode.updatedAt = new Date().toISOString();
});
return { albumId, reviewStatus: 'approved' };
}
async listAllDramas(): Promise<DramaRecord[]> {
if (this.hasDatabase()) {
const records = await this.dataSource
.getRepository(Drama)
.find({ order: { sortOrder: 'DESC', id: 'DESC' } });
return Promise.all(records.map((drama) => this.toDramaRecordWithCounts(drama)));
}
return this.sortDramas(this.dramas.map((drama) => this.withEpisodeCounts(drama)));
}
async listAllEpisodes(): Promise<EpisodeRecord[]> {
if (this.hasDatabase()) {
const records = await this.dataSource
.getRepository(Episode)
.find({ order: { id: 'ASC' } });
return records.map((episode) => this.toEpisodeRecord(episode));
}
return [...this.episodes].sort((left, right) => left.id - right.id);
}
async updateEpisodePublishStatus(
episodeId: number,
publishStatus: PublishStatus,
): Promise<EpisodeRecord> {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(Episode);
const episode = await repository.findOne({ where: { id: episodeId } });
if (!episode) {
throw new NotFoundException('Episode not found');
}
episode.publishStatus = publishStatus;
episode.status = publishStatus;
const saved = await repository.save(episode);
return this.toEpisodeRecord(saved);
}
const episode = await this.getMemEpisodeOrThrow(episodeId);
episode.publishStatus = publishStatus;
episode.status = publishStatus;
episode.updatedAt = new Date().toISOString();
return episode;
}
private hasDatabase(): this is this & { dataSource: DataSource } {
return Boolean(this.dataSource?.isInitialized);
}
private getMemEpisodeOrThrow(episodeId: number): EpisodeRecord {
const episode = this.episodes.find((item) => item.id === episodeId);
if (!episode) {
throw new NotFoundException('Episode not found');
}
return episode;
}
private async toDramaRecordWithCounts(entity: Drama): Promise<DramaRecord> {
const episodeRepository = this.dataSource!.getRepository(Episode);
const dramaId = Number(entity.id);
const totalEpisodes = await episodeRepository.count({ where: { dramaId } });
const publishedEpisodes = await episodeRepository.count({
where: {
dramaId,
publishStatus: PublishStatus.Published,
reviewStatus: 'approved',
},
});
return this.toDramaRecord(entity, totalEpisodes, publishedEpisodes);
}
private toDramaRecord(
entity: Drama,
totalEpisodes: number,
publishedEpisodes: number,
): DramaRecord {
return {
id: Number(entity.id),
tiktokAlbumId: entity.tiktokAlbumId,
title: entity.title,
description: entity.description,
coverId: entity.coverId,
coverUrl: entity.coverUrl,
portraitCoverUrl: entity.portraitCoverUrl,
landscapeCoverUrl: entity.landscapeCoverUrl,
type: entity.type,
tags: entity.tags ?? [],
region: entity.region,
language: entity.language,
year: entity.year,
status: entity.status,
sortOrder: entity.sortOrder,
isRecommended: entity.isRecommended,
onlineVersion: entity.onlineVersion,
totalEpisodes,
publishedEpisodes,
createdAt: this.toIso(entity.createdAt),
updatedAt: this.toIso(entity.updatedAt),
};
}
private toEpisodeRecord(entity: Episode): EpisodeRecord {
const dramaId = Number(entity.dramaId);
return {
id: Number(entity.id),
dramaId,
albumId: dramaId,
tiktokEpisodeId: entity.tiktokEpisodeId,
episodeNo: entity.episodeNo,
seq: entity.episodeNo,
title: entity.title,
description: entity.description,
byteplusVid: entity.byteplusVid,
coverId: entity.coverId,
coverUrl: entity.coverUrl,
videoUrl: entity.videoUrl,
storageBucket: entity.storageBucket,
objectKey: entity.objectKey,
trialDurationSeconds: entity.trialDurationSeconds,
durationSeconds: entity.durationSeconds,
width: entity.width,
height: entity.height,
isPaid: entity.isPaid,
freeType: entity.freeType,
unlockPrice: entity.unlockPrice,
price: entity.price,
publishAt: this.toIsoOrUndefined(entity.publishAt),
nextReleaseAt: this.toIsoOrUndefined(entity.nextReleaseAt),
status: entity.status,
reviewStatus: entity.reviewStatus,
reviewMessage: entity.reviewMessage,
publishStatus: entity.publishStatus,
createdAt: this.toIso(entity.createdAt),
updatedAt: this.toIso(entity.updatedAt),
};
}
private toIso(value?: Date | string): string {
if (!value) {
return new Date().toISOString();
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}
private toIsoOrUndefined(value?: Date | string): string | undefined {
if (!value) {
return undefined;
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}
private resolvePagination(input: PaginationInput) {
const page = Math.max(input.page, 1);
const pageSize = Math.min(Math.max(input.pageSize, 1), 100);
return { page, pageSize, skip: (page - 1) * pageSize, take: pageSize };
}
private sortDramas(records: DramaRecord[]) {
return [...records].sort((left, right) => {
if (right.sortOrder !== left.sortOrder) {
return right.sortOrder - left.sortOrder;
}
return right.id - left.id;
});
}
private withEpisodeCounts(drama: DramaRecord): DramaRecord {
const episodes = this.episodes.filter((episode) => episode.dramaId === drama.id);
return {
...drama,
totalEpisodes: episodes.length,
publishedEpisodes: episodes.filter(
(episode) =>
episode.publishStatus === PublishStatus.Published &&
episode.reviewStatus === 'approved',
).length,
};
}
private paginate<T>(
records: T[],
input: PaginationInput,
): PaginatedResponse<T> {
const page = Math.max(input.page, 1);
const pageSize = Math.min(Math.max(input.pageSize, 1), 100);
const start = (page - 1) * pageSize;
return {
list: records.slice(start, start + pageSize),
pagination: {
page,
pageSize,
total: records.length,
},
};
}
}