chore: 初始化TikTok短剧后端项目基础架构
创建了完整的NestJS项目结构,包含配置文件、核心模块、工具类、拦截器、过滤器等基础代码,实现了健康检查、用户认证、管理员RBAC、剧集管理、订单支付、媒体管理等核心功能的基础框架,配置了ESLint、TypeScript、Jest等开发工具链。
This commit is contained in:
221
src/modules/dramas/dramas.service.ts
Normal file
221
src/modules/dramas/dramas.service.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
||||
import { CreateDramaDto } from './dto/create-drama.dto';
|
||||
import { CreateEpisodeDto } from './dto/create-episode.dto';
|
||||
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[] = [];
|
||||
|
||||
createDrama(dto: CreateDramaDto): DramaRecord {
|
||||
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,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.dramas.push(drama);
|
||||
return drama;
|
||||
}
|
||||
|
||||
listAdminDramas(pagination: PaginationInput): PaginatedResponse<DramaRecord> {
|
||||
return this.paginate(this.sortDramas(this.dramas), pagination);
|
||||
}
|
||||
|
||||
createEpisode(dto: CreateEpisodeDto): EpisodeRecord {
|
||||
const dramaId = dto.dramaId ?? dto.albumId;
|
||||
const episodeNo = dto.episodeNo ?? dto.seq;
|
||||
if (!dramaId || !episodeNo) {
|
||||
throw new NotFoundException('Drama not found');
|
||||
}
|
||||
|
||||
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 freeType = dto.freeType ?? (dto.isPaid ? 'paid' : 'free');
|
||||
const price = dto.price ?? dto.unlockPrice ?? 0;
|
||||
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,
|
||||
isPaid: freeType === 'paid',
|
||||
freeType,
|
||||
unlockPrice: price,
|
||||
price,
|
||||
publishAt: dto.publishAt,
|
||||
nextReleaseAt: dto.nextReleaseAt,
|
||||
status: dto.status ?? dto.publishStatus ?? PublishStatus.Draft,
|
||||
reviewStatus: dto.reviewStatus ?? 'pending',
|
||||
publishStatus: dto.publishStatus ?? dto.status ?? PublishStatus.Draft,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.episodes.push(episode);
|
||||
return episode;
|
||||
}
|
||||
|
||||
listPublishedDramas(
|
||||
pagination: PaginationInput,
|
||||
): PaginatedResponse<DramaRecord> {
|
||||
return this.paginate(
|
||||
this.sortDramas(
|
||||
this.dramas.filter((item) => item.status === PublishStatus.Published),
|
||||
),
|
||||
pagination,
|
||||
);
|
||||
}
|
||||
|
||||
listPublishedEpisodes(
|
||||
dramaId: number,
|
||||
pagination: PaginationInput,
|
||||
): PaginatedResponse<EpisodeRecord> {
|
||||
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,
|
||||
)
|
||||
.sort((left, right) => left.episodeNo - right.episodeNo);
|
||||
|
||||
return this.paginate(episodes, pagination);
|
||||
}
|
||||
|
||||
getPublishedEpisodeOrThrow(episodeId: number): EpisodeRecord {
|
||||
const episode = this.episodes.find((item) => item.id === episodeId);
|
||||
if (!episode || episode.status !== PublishStatus.Published) {
|
||||
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;
|
||||
}
|
||||
|
||||
getEpisodeOrThrow(episodeId: number): EpisodeRecord {
|
||||
const episode = this.episodes.find((item) => item.id === episodeId);
|
||||
if (!episode) {
|
||||
throw new NotFoundException('Episode not found');
|
||||
}
|
||||
|
||||
return episode;
|
||||
}
|
||||
|
||||
submitReview(albumId: number) {
|
||||
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 = 'pending';
|
||||
episode.updatedAt = new Date().toISOString();
|
||||
});
|
||||
|
||||
return {
|
||||
albumId,
|
||||
reviewStatus: 'pending',
|
||||
};
|
||||
}
|
||||
|
||||
updateEpisodePublishStatus(episodeId: number, publishStatus: PublishStatus) {
|
||||
const episode = this.getEpisodeOrThrow(episodeId);
|
||||
episode.publishStatus = publishStatus;
|
||||
episode.status = publishStatus;
|
||||
episode.updatedAt = new Date().toISOString();
|
||||
return episode;
|
||||
}
|
||||
|
||||
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 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user