refactor: 重构项目支持TypeORM数据库持久化
新增数据库实体类与服务实现,将内存存储扩展为支持数据库的完整实现,包括: 1. 新增Drama和Episode实体类,添加多个业务字段 2. 重构DramasService实现数据库CRUD与业务逻辑 3. 升级相关服务类为异步方法适配数据库操作 4. 更新测试用例适配异步接口变更 5. 完善分页、数据转换等辅助工具方法
This commit is contained in:
@@ -70,13 +70,13 @@ export class DataService {
|
|||||||
|
|
||||||
async sync(input: SyncInput = {}) {
|
async sync(input: SyncInput = {}) {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const dramas = this.dramasService
|
const dramas = (await this.dramasService.listAllDramas()).filter(
|
||||||
.listAllDramas()
|
(drama) => !input.dramaId || drama.id === input.dramaId,
|
||||||
.filter((drama) => !input.dramaId || drama.id === input.dramaId);
|
);
|
||||||
const dramaIds = new Set(dramas.map((drama) => drama.id));
|
const dramaIds = new Set(dramas.map((drama) => drama.id));
|
||||||
const episodes = this.dramasService
|
const episodes = (await this.dramasService.listAllEpisodes()).filter(
|
||||||
.listAllEpisodes()
|
(episode) => dramaIds.has(episode.dramaId),
|
||||||
.filter((episode) => dramaIds.has(episode.dramaId));
|
);
|
||||||
|
|
||||||
const dramaRecords = dramas.map((drama) => ({
|
const dramaRecords = dramas.map((drama) => ({
|
||||||
dramaId: drama.id,
|
dramaId: drama.id,
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import {
|
|||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
Optional,
|
||||||
UnprocessableEntityException,
|
UnprocessableEntityException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
||||||
import { ConfigureEpisodesDto } from './dto/configure-episodes.dto';
|
import { ConfigureEpisodesDto } from './dto/configure-episodes.dto';
|
||||||
import { CreateDramaDto } from './dto/create-drama.dto';
|
import { CreateDramaDto } from './dto/create-drama.dto';
|
||||||
import { CreateEpisodeDto } from './dto/create-episode.dto';
|
import { CreateEpisodeDto } from './dto/create-episode.dto';
|
||||||
import { UpdateDramaDto } from './dto/update-drama.dto';
|
import { UpdateDramaDto } from './dto/update-drama.dto';
|
||||||
import { UpdateEpisodeDto } from './dto/update-episode.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 { PublishStatus } from './drama-status.enum';
|
||||||
import {
|
import {
|
||||||
DramaRecord,
|
DramaRecord,
|
||||||
@@ -28,7 +33,37 @@ export class DramasService {
|
|||||||
private readonly dramas: DramaRecord[] = [];
|
private readonly dramas: DramaRecord[] = [];
|
||||||
private readonly episodes: EpisodeRecord[] = [];
|
private readonly episodes: EpisodeRecord[] = [];
|
||||||
|
|
||||||
createDrama(dto: CreateDramaDto): DramaRecord {
|
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 now = new Date().toISOString();
|
||||||
const drama: DramaRecord = {
|
const drama: DramaRecord = {
|
||||||
id: this.dramaSequence++,
|
id: this.dramaSequence++,
|
||||||
@@ -58,11 +93,41 @@ export class DramasService {
|
|||||||
return drama;
|
return drama;
|
||||||
}
|
}
|
||||||
|
|
||||||
listAdminDramas(pagination: PaginationInput): PaginatedResponse<DramaRecord> {
|
async listAdminDramas(
|
||||||
return this.paginate(this.sortDramas(this.dramas.map((drama) => this.withEpisodeCounts(drama))), pagination);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDramaOrThrow(id: number): DramaRecord {
|
|
||||||
const drama = this.dramas.find((item) => item.id === id);
|
const drama = this.dramas.find((item) => item.id === id);
|
||||||
if (!drama) {
|
if (!drama) {
|
||||||
throw new NotFoundException('Drama not found');
|
throw new NotFoundException('Drama not found');
|
||||||
@@ -71,7 +136,18 @@ export class DramasService {
|
|||||||
return this.withEpisodeCounts(drama);
|
return this.withEpisodeCounts(drama);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDrama(id: number, dto: UpdateDramaDto): DramaRecord {
|
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);
|
const drama = this.dramas.find((item) => item.id === id);
|
||||||
if (!drama) {
|
if (!drama) {
|
||||||
throw new NotFoundException('Drama not found');
|
throw new NotFoundException('Drama not found');
|
||||||
@@ -85,30 +161,78 @@ export class DramasService {
|
|||||||
return this.withEpisodeCounts(drama);
|
return this.withEpisodeCounts(drama);
|
||||||
}
|
}
|
||||||
|
|
||||||
createEpisode(dto: CreateEpisodeDto): EpisodeRecord {
|
async createEpisode(dto: CreateEpisodeDto): Promise<EpisodeRecord> {
|
||||||
const dramaId = dto.dramaId ?? dto.albumId;
|
const dramaId = dto.dramaId ?? dto.albumId;
|
||||||
const episodeNo = dto.episodeNo ?? dto.seq;
|
const episodeNo = dto.episodeNo ?? dto.seq;
|
||||||
if (!dramaId || !episodeNo) {
|
if (!dramaId || !episodeNo) {
|
||||||
throw new NotFoundException('Drama not found');
|
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);
|
const drama = this.dramas.find((item) => item.id === dramaId);
|
||||||
if (!drama) {
|
if (!drama) {
|
||||||
throw new NotFoundException('Drama not found');
|
throw new NotFoundException('Drama not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicate = this.episodes.some(
|
const duplicate = this.episodes.some(
|
||||||
(item) =>
|
(item) => item.dramaId === dramaId && item.episodeNo === episodeNo,
|
||||||
item.dramaId === dramaId && item.episodeNo === episodeNo,
|
|
||||||
);
|
);
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
throw new ConflictException('Episode number already exists in this drama');
|
throw new ConflictException('Episode number already exists in this drama');
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const freeType = dto.freeType ?? (dto.isPaid ? 'paid' : 'free');
|
|
||||||
const price = dto.price ?? dto.unlockPrice ?? 0;
|
|
||||||
const publishStatus = dto.publishStatus ?? dto.status ?? PublishStatus.Draft;
|
|
||||||
const episode: EpisodeRecord = {
|
const episode: EpisodeRecord = {
|
||||||
id: this.episodeSequence++,
|
id: this.episodeSequence++,
|
||||||
dramaId,
|
dramaId,
|
||||||
@@ -133,9 +257,7 @@ export class DramasService {
|
|||||||
publishAt: dto.publishAt,
|
publishAt: dto.publishAt,
|
||||||
nextReleaseAt: dto.nextReleaseAt,
|
nextReleaseAt: dto.nextReleaseAt,
|
||||||
status: publishStatus,
|
status: publishStatus,
|
||||||
reviewStatus:
|
reviewStatus,
|
||||||
dto.reviewStatus ??
|
|
||||||
(publishStatus === PublishStatus.Published ? 'approved' : 'not_submitted'),
|
|
||||||
publishStatus,
|
publishStatus,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -145,10 +267,69 @@ export class DramasService {
|
|||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
configureEpisodes(
|
async configureEpisodes(
|
||||||
dramaId: number,
|
dramaId: number,
|
||||||
dto: ConfigureEpisodesDto,
|
dto: ConfigureEpisodesDto,
|
||||||
): PaginatedResponse<EpisodeRecord> {
|
): 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);
|
const drama = this.dramas.find((item) => item.id === dramaId);
|
||||||
if (!drama) {
|
if (!drama) {
|
||||||
throw new NotFoundException('Drama not found');
|
throw new NotFoundException('Drama not found');
|
||||||
@@ -179,8 +360,12 @@ export class DramasService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let episodeNo = existing.length + 1; episodeNo <= dto.episodeCount; episodeNo++) {
|
for (
|
||||||
this.createEpisode({
|
let episodeNo = existing.length + 1;
|
||||||
|
episodeNo <= dto.episodeCount;
|
||||||
|
episodeNo++
|
||||||
|
) {
|
||||||
|
await this.createEpisode({
|
||||||
dramaId,
|
dramaId,
|
||||||
episodeNo,
|
episodeNo,
|
||||||
title: `第${episodeNo}集`,
|
title: `第${episodeNo}集`,
|
||||||
@@ -199,28 +384,79 @@ export class DramasService {
|
|||||||
.sort((left, right) => left.episodeNo - right.episodeNo);
|
.sort((left, right) => left.episodeNo - right.episodeNo);
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
pagination: {
|
pagination: { page: 1, pageSize: list.length, total: list.length },
|
||||||
page: 1,
|
|
||||||
pageSize: list.length,
|
|
||||||
total: list.length,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
listAdminEpisodes(
|
async listAdminEpisodes(
|
||||||
dramaId: number,
|
dramaId: number,
|
||||||
pagination: PaginationInput,
|
pagination: PaginationInput,
|
||||||
): PaginatedResponse<EpisodeRecord> {
|
): Promise<PaginatedResponse<EpisodeRecord>> {
|
||||||
this.getDramaOrThrow(dramaId);
|
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
|
const records = this.episodes
|
||||||
.filter((episode) => episode.dramaId === dramaId)
|
.filter((episode) => episode.dramaId === dramaId)
|
||||||
.sort((left, right) => left.episodeNo - right.episodeNo);
|
.sort((left, right) => left.episodeNo - right.episodeNo);
|
||||||
return this.paginate(records, pagination);
|
return this.paginate(records, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEpisode(id: number, dto: UpdateEpisodeDto): EpisodeRecord {
|
async updateEpisode(id: number, dto: UpdateEpisodeDto): Promise<EpisodeRecord> {
|
||||||
const episode = this.getEpisodeOrThrow(id);
|
if (this.hasDatabase()) {
|
||||||
const freeType = dto.freeType ?? (dto.isPaid === undefined ? episode.freeType : dto.isPaid ? 'paid' : 'free');
|
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, {
|
Object.assign(episode, {
|
||||||
...dto,
|
...dto,
|
||||||
dramaId: dto.dramaId ?? dto.albumId ?? episode.dramaId,
|
dramaId: dto.dramaId ?? dto.albumId ?? episode.dramaId,
|
||||||
@@ -238,7 +474,7 @@ export class DramasService {
|
|||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachEpisodeMedia(input: {
|
async attachEpisodeMedia(input: {
|
||||||
episodeId: number;
|
episodeId: number;
|
||||||
byteplusVid: string;
|
byteplusVid: string;
|
||||||
videoUrl: string;
|
videoUrl: string;
|
||||||
@@ -249,8 +485,29 @@ export class DramasService {
|
|||||||
height: number;
|
height: number;
|
||||||
reviewStatus: EpisodeRecord['reviewStatus'];
|
reviewStatus: EpisodeRecord['reviewStatus'];
|
||||||
reviewMessage?: string;
|
reviewMessage?: string;
|
||||||
}): EpisodeRecord {
|
}): Promise<EpisodeRecord> {
|
||||||
const episode = this.getEpisodeOrThrow(input.episodeId);
|
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.byteplusVid = input.byteplusVid;
|
||||||
episode.videoUrl = input.videoUrl;
|
episode.videoUrl = input.videoUrl;
|
||||||
episode.storageBucket = input.storageBucket;
|
episode.storageBucket = input.storageBucket;
|
||||||
@@ -264,21 +521,49 @@ export class DramasService {
|
|||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEpisodeReviewStatus(
|
async updateEpisodeReviewStatus(
|
||||||
episodeId: number,
|
episodeId: number,
|
||||||
reviewStatus: EpisodeRecord['reviewStatus'],
|
reviewStatus: EpisodeRecord['reviewStatus'],
|
||||||
reviewMessage?: string,
|
reviewMessage?: string,
|
||||||
): EpisodeRecord {
|
): Promise<EpisodeRecord> {
|
||||||
const episode = this.getEpisodeOrThrow(episodeId);
|
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.reviewStatus = reviewStatus;
|
||||||
episode.reviewMessage = reviewMessage;
|
episode.reviewMessage = reviewMessage;
|
||||||
episode.updatedAt = new Date().toISOString();
|
episode.updatedAt = new Date().toISOString();
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
listPublishedDramas(
|
async listPublishedDramas(
|
||||||
pagination: PaginationInput,
|
pagination: PaginationInput,
|
||||||
): PaginatedResponse<DramaRecord> {
|
): 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(
|
return this.paginate(
|
||||||
this.sortDramas(
|
this.sortDramas(
|
||||||
this.dramas
|
this.dramas
|
||||||
@@ -289,7 +574,17 @@ export class DramasService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPublishedDramaOrThrow(id: number): DramaRecord {
|
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(
|
const drama = this.dramas.find(
|
||||||
(item) => item.id === id && item.status === PublishStatus.Published,
|
(item) => item.id === id && item.status === PublishStatus.Published,
|
||||||
);
|
);
|
||||||
@@ -300,10 +595,37 @@ export class DramasService {
|
|||||||
return this.withEpisodeCounts(drama);
|
return this.withEpisodeCounts(drama);
|
||||||
}
|
}
|
||||||
|
|
||||||
listPublishedEpisodes(
|
async listPublishedEpisodes(
|
||||||
dramaId: number,
|
dramaId: number,
|
||||||
pagination: PaginationInput,
|
pagination: PaginationInput,
|
||||||
): PaginatedResponse<EpisodeRecord> {
|
): 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);
|
const drama = this.dramas.find((item) => item.id === dramaId);
|
||||||
if (!drama || drama.status !== PublishStatus.Published) {
|
if (!drama || drama.status !== PublishStatus.Published) {
|
||||||
throw new NotFoundException('Drama not found');
|
throw new NotFoundException('Drama not found');
|
||||||
@@ -324,7 +646,30 @@ export class DramasService {
|
|||||||
return this.paginate(episodes, pagination);
|
return this.paginate(episodes, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPublishedEpisodeOrThrow(episodeId: number): EpisodeRecord {
|
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);
|
const episode = this.episodes.find((item) => item.id === episodeId);
|
||||||
if (
|
if (
|
||||||
!episode ||
|
!episode ||
|
||||||
@@ -343,16 +688,39 @@ export class DramasService {
|
|||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEpisodeOrThrow(episodeId: number): EpisodeRecord {
|
async getEpisodeOrThrow(episodeId: number): Promise<EpisodeRecord> {
|
||||||
const episode = this.episodes.find((item) => item.id === episodeId);
|
if (this.hasDatabase()) {
|
||||||
|
const episode = await this.dataSource
|
||||||
|
.getRepository(Episode)
|
||||||
|
.findOne({ where: { id: episodeId } });
|
||||||
if (!episode) {
|
if (!episode) {
|
||||||
throw new NotFoundException('Episode not found');
|
throw new NotFoundException('Episode not found');
|
||||||
}
|
}
|
||||||
|
return this.toEpisodeRecord(episode);
|
||||||
return 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' };
|
||||||
}
|
}
|
||||||
|
|
||||||
submitReview(albumId: number) {
|
|
||||||
const album = this.dramas.find((item) => item.id === albumId);
|
const album = this.dramas.find((item) => item.id === albumId);
|
||||||
if (!album) {
|
if (!album) {
|
||||||
throw new NotFoundException('Drama not found');
|
throw new NotFoundException('Drama not found');
|
||||||
@@ -366,28 +734,167 @@ export class DramasService {
|
|||||||
episode.updatedAt = new Date().toISOString();
|
episode.updatedAt = new Date().toISOString();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { albumId, reviewStatus: 'approved' };
|
||||||
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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
listAllDramas() {
|
|
||||||
return this.sortDramas(this.dramas.map((drama) => this.withEpisodeCounts(drama)));
|
return this.sortDramas(this.dramas.map((drama) => this.withEpisodeCounts(drama)));
|
||||||
}
|
}
|
||||||
|
|
||||||
listAllEpisodes() {
|
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);
|
return [...this.episodes].sort((left, right) => left.id - right.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEpisodePublishStatus(episodeId: number, publishStatus: PublishStatus) {
|
async updateEpisodePublishStatus(
|
||||||
const episode = this.getEpisodeOrThrow(episodeId);
|
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.publishStatus = publishStatus;
|
||||||
episode.status = publishStatus;
|
episode.status = publishStatus;
|
||||||
episode.updatedAt = new Date().toISOString();
|
episode.updatedAt = new Date().toISOString();
|
||||||
return episode;
|
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[]) {
|
private sortDramas(records: DramaRecord[]) {
|
||||||
return [...records].sort((left, right) => {
|
return [...records].sort((left, right) => {
|
||||||
if (right.sortOrder !== left.sortOrder) {
|
if (right.sortOrder !== left.sortOrder) {
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ export class Drama {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 128, nullable: true })
|
||||||
|
tiktokAlbumId?: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 255 })
|
@Column({ type: 'varchar', length: 255 })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
coverId?: number;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 1024 })
|
@Column({ type: 'varchar', length: 1024 })
|
||||||
coverUrl: string;
|
coverUrl: string;
|
||||||
|
|
||||||
@@ -51,6 +57,9 @@ export class Drama {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isRecommended: boolean;
|
isRecommended: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 1 })
|
||||||
|
onlineVersion: number;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export class Episode {
|
|||||||
@Column({ type: 'bigint' })
|
@Column({ type: 'bigint' })
|
||||||
dramaId: number;
|
dramaId: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 128, nullable: true })
|
||||||
|
tiktokEpisodeId?: string;
|
||||||
|
|
||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
episodeNo: number;
|
episodeNo: number;
|
||||||
|
|
||||||
@@ -26,16 +29,28 @@ export class Episode {
|
|||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
coverId?: number;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 1024 })
|
@Column({ type: 'varchar', length: 1024 })
|
||||||
coverUrl: string;
|
coverUrl: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 1024 })
|
@Column({ type: 'varchar', length: 1024, nullable: true })
|
||||||
videoUrl: string;
|
videoUrl?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 128, nullable: true })
|
||||||
|
byteplusVid?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
|
storageBucket?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 512, nullable: true })
|
||||||
|
objectKey?: string;
|
||||||
|
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
trialDurationSeconds?: number;
|
trialDurationSeconds?: number;
|
||||||
|
|
||||||
@Column({ type: 'int' })
|
@Column({ type: 'int', default: 0 })
|
||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
|
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
@@ -47,9 +62,15 @@ export class Episode {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isPaid: boolean;
|
isPaid: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 16, default: 'free' })
|
||||||
|
freeType: 'free' | 'paid';
|
||||||
|
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
unlockPrice?: number;
|
unlockPrice?: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 0 })
|
||||||
|
price: number;
|
||||||
|
|
||||||
@Column({ type: 'datetime', nullable: true })
|
@Column({ type: 'datetime', nullable: true })
|
||||||
publishAt?: Date;
|
publishAt?: Date;
|
||||||
|
|
||||||
@@ -59,6 +80,15 @@ export class Episode {
|
|||||||
@Column({ type: 'varchar', length: 32, default: PublishStatus.Draft })
|
@Column({ type: 'varchar', length: 32, default: PublishStatus.Draft })
|
||||||
status: PublishStatus;
|
status: PublishStatus;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 32, default: 'not_submitted' })
|
||||||
|
reviewStatus: 'not_submitted' | 'pending' | 'reviewing' | 'approved' | 'rejected';
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 512, nullable: true })
|
||||||
|
reviewMessage?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 32, default: PublishStatus.Draft })
|
||||||
|
publishStatus: PublishStatus;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,10 @@ export class MediaService {
|
|||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
createDirectUploadTask(dto: CreateDirectUploadTaskDto): MediaUploadJobRecord {
|
async createDirectUploadTask(
|
||||||
this.dramasService.getEpisodeOrThrow(dto.episodeId);
|
dto: CreateDirectUploadTaskDto,
|
||||||
|
): Promise<MediaUploadJobRecord> {
|
||||||
|
await this.dramasService.getEpisodeOrThrow(dto.episodeId);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const jobId = randomUUID();
|
const jobId = randomUUID();
|
||||||
const upload = this.bytePlusMediaProvider.createDirectUpload({
|
const upload = this.bytePlusMediaProvider.createDirectUpload({
|
||||||
@@ -87,7 +89,7 @@ export class MediaService {
|
|||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
completeDirectUploadTask(jobId: string): MediaUploadJobRecord {
|
async completeDirectUploadTask(jobId: string): Promise<MediaUploadJobRecord> {
|
||||||
const job = this.getUploadJobOrThrow(jobId);
|
const job = this.getUploadJobOrThrow(jobId);
|
||||||
if (!job.objectKey || !job.episodeId) {
|
if (!job.objectKey || !job.episodeId) {
|
||||||
throw new NotFoundException('Upload task not found');
|
throw new NotFoundException('Upload task not found');
|
||||||
@@ -116,7 +118,7 @@ export class MediaService {
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dramasService.attachEpisodeMedia({
|
await this.dramasService.attachEpisodeMedia({
|
||||||
episodeId: job.episodeId,
|
episodeId: job.episodeId,
|
||||||
byteplusVid: completed.byteplusVid,
|
byteplusVid: completed.byteplusVid,
|
||||||
videoUrl: completed.videoUrl,
|
videoUrl: completed.videoUrl,
|
||||||
@@ -132,7 +134,7 @@ export class MediaService {
|
|||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
retryReview(jobId: string): MediaUploadJobRecord {
|
async retryReview(jobId: string): Promise<MediaUploadJobRecord> {
|
||||||
const job = this.getUploadJobOrThrow(jobId);
|
const job = this.getUploadJobOrThrow(jobId);
|
||||||
if (!job.byteplusVid || !job.episodeId) {
|
if (!job.byteplusVid || !job.episodeId) {
|
||||||
throw new NotFoundException('Upload task not found');
|
throw new NotFoundException('Upload task not found');
|
||||||
@@ -143,7 +145,7 @@ export class MediaService {
|
|||||||
job.reviewStatus = review.reviewStatus;
|
job.reviewStatus = review.reviewStatus;
|
||||||
job.reviewMessage = review.reviewMessage;
|
job.reviewMessage = review.reviewMessage;
|
||||||
job.updatedAt = new Date().toISOString();
|
job.updatedAt = new Date().toISOString();
|
||||||
this.dramasService.updateEpisodeReviewStatus(
|
await this.dramasService.updateEpisodeReviewStatus(
|
||||||
job.episodeId,
|
job.episodeId,
|
||||||
review.reviewStatus,
|
review.reviewStatus,
|
||||||
review.reviewMessage,
|
review.reviewMessage,
|
||||||
@@ -151,7 +153,7 @@ export class MediaService {
|
|||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
syncReviewStatuses() {
|
async syncReviewStatuses() {
|
||||||
const reviewingJobs = this.uploadJobs.filter(
|
const reviewingJobs = this.uploadJobs.filter(
|
||||||
(job) =>
|
(job) =>
|
||||||
job.byteplusVid &&
|
job.byteplusVid &&
|
||||||
@@ -167,7 +169,7 @@ export class MediaService {
|
|||||||
job.reviewStatus = review.reviewStatus;
|
job.reviewStatus = review.reviewStatus;
|
||||||
job.reviewMessage = review.reviewMessage;
|
job.reviewMessage = review.reviewMessage;
|
||||||
job.updatedAt = new Date().toISOString();
|
job.updatedAt = new Date().toISOString();
|
||||||
this.dramasService.updateEpisodeReviewStatus(
|
await this.dramasService.updateEpisodeReviewStatus(
|
||||||
job.episodeId as number,
|
job.episodeId as number,
|
||||||
review.reviewStatus,
|
review.reviewStatus,
|
||||||
review.reviewMessage,
|
review.reviewMessage,
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ export class OrdersService {
|
|||||||
|
|
||||||
constructor(private readonly dramasService: DramasService) {}
|
constructor(private readonly dramasService: DramasService) {}
|
||||||
|
|
||||||
createOrder(user: UserRecord, dto: CreateOrderDto): OrderRecord {
|
async createOrder(user: UserRecord, dto: CreateOrderDto): Promise<OrderRecord> {
|
||||||
const episode = this.dramasService.getPublishedEpisodeOrThrow(dto.episodeId);
|
const episode = await this.dramasService.getPublishedEpisodeOrThrow(
|
||||||
|
dto.episodeId,
|
||||||
|
);
|
||||||
if (!episode.isPaid) {
|
if (!episode.isPaid) {
|
||||||
throw new UnprocessableEntityException('Episode is free');
|
throw new UnprocessableEntityException('Episode is free');
|
||||||
}
|
}
|
||||||
@@ -46,8 +48,8 @@ export class OrdersService {
|
|||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayback(user: UserRecord, episodeId: number) {
|
async getPlayback(user: UserRecord, episodeId: number) {
|
||||||
const episode = this.dramasService.getPublishedEpisodeOrThrow(episodeId);
|
const episode = await this.dramasService.getPublishedEpisodeOrThrow(episodeId);
|
||||||
const isUnlocked =
|
const isUnlocked =
|
||||||
!episode.isPaid ||
|
!episode.isPaid ||
|
||||||
this.unlocks.some(
|
this.unlocks.some(
|
||||||
@@ -107,8 +109,8 @@ export class OrdersService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
hasEpisodePermission(userId: number, episodeId: number) {
|
async hasEpisodePermission(userId: number, episodeId: number) {
|
||||||
const episode = this.dramasService.getPublishedEpisodeOrThrow(episodeId);
|
const episode = await this.dramasService.getPublishedEpisodeOrThrow(episodeId);
|
||||||
return !episode.isPaid || this.isUnlocked(userId, episodeId);
|
return !episode.isPaid || this.isUnlocked(userId, episodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ export class PlayerService {
|
|||||||
private readonly ordersService: OrdersService,
|
private readonly ordersService: OrdersService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getEpisodePlayer(user: UserRecord, episodeId: number) {
|
async getEpisodePlayer(user: UserRecord, episodeId: number) {
|
||||||
const episode = this.dramasService.getPublishedEpisodeOrThrow(episodeId);
|
const episode = await this.dramasService.getPublishedEpisodeOrThrow(episodeId);
|
||||||
const hasPermission = this.ordersService.hasEpisodePermission(
|
const hasPermission = await this.ordersService.hasEpisodePermission(
|
||||||
user.id,
|
user.id,
|
||||||
episode.id,
|
episode.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -160,13 +160,13 @@ describe('database-backed persistence', () => {
|
|||||||
dataSource as never,
|
dataSource as never,
|
||||||
);
|
);
|
||||||
|
|
||||||
const drama = dramasService.createDrama({
|
const drama = await dramasService.createDrama({
|
||||||
title: 'DB Metrics',
|
title: 'DB Metrics',
|
||||||
coverUrl: 'https://cdn.example.com/cover.jpg',
|
coverUrl: 'https://cdn.example.com/cover.jpg',
|
||||||
type: 'action',
|
type: 'action',
|
||||||
status: PublishStatus.Published,
|
status: PublishStatus.Published,
|
||||||
});
|
});
|
||||||
dramasService.createEpisode({
|
await dramasService.createEpisode({
|
||||||
dramaId: drama.id,
|
dramaId: drama.id,
|
||||||
episodeNo: 1,
|
episodeNo: 1,
|
||||||
title: 'DB Metrics Episode',
|
title: 'DB Metrics Episode',
|
||||||
|
|||||||
Reference in New Issue
Block a user