chore: 初始化TikTok短剧后端项目基础架构

创建了完整的NestJS项目结构,包含配置文件、核心模块、工具类、拦截器、过滤器等基础代码,实现了健康检查、用户认证、管理员RBAC、剧集管理、订单支付、媒体管理等核心功能的基础框架,配置了ESLint、TypeScript、Jest等开发工具链。
This commit is contained in:
2026-07-01 13:55:37 +08:00
commit ef8c31d3be
92 changed files with 11462 additions and 0 deletions

View 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,
},
};
}
}