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,147 @@
import {
Injectable,
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
import { DramasService } from '../dramas/dramas.service';
import { UserRecord } from '../users/interfaces/user-record.interface';
import { CreateOrderDto } from './dto/create-order.dto';
import { TikTokPaymentWebhookDto } from './dto/tiktok-payment-webhook.dto';
import {
OrderRecord,
UserEpisodeUnlockRecord,
} from './interfaces/order-records.interface';
@Injectable()
export class OrdersService {
private orderSequence = 1;
private unlockSequence = 1;
private readonly orders: OrderRecord[] = [];
private readonly unlocks: UserEpisodeUnlockRecord[] = [];
constructor(private readonly dramasService: DramasService) {}
createOrder(user: UserRecord, dto: CreateOrderDto): OrderRecord {
const episode = this.dramasService.getPublishedEpisodeOrThrow(dto.episodeId);
if (!episode.isPaid) {
throw new UnprocessableEntityException('Episode is free');
}
const now = new Date().toISOString();
const order: OrderRecord = {
id: this.orderSequence++,
orderNo: this.createOrderNo(),
tradeOrderId: this.createTradeOrderId(),
userId: user.id,
albumId: episode.albumId,
episodeId: episode.id,
amount: episode.price ?? episode.unlockPrice ?? 0,
status: 'pending',
createdAt: now,
updatedAt: now,
};
this.orders.push(order);
return order;
}
getPlayback(user: UserRecord, episodeId: number) {
const episode = this.dramasService.getPublishedEpisodeOrThrow(episodeId);
const isUnlocked =
!episode.isPaid ||
this.unlocks.some(
(unlock) => unlock.userId === user.id && unlock.episodeId === episodeId,
);
return {
episodeId: episode.id,
dramaId: episode.dramaId,
albumId: episode.albumId,
isLocked: !isUnlocked,
isPaid: episode.isPaid,
trialDurationSeconds: episode.trialDurationSeconds,
durationSeconds: episode.durationSeconds,
videoUrl: isUnlocked ? episode.videoUrl : null,
};
}
handlePaymentWebhook(dto: TikTokPaymentWebhookDto) {
const order = this.orders.find(
(item) =>
item.orderNo === dto.orderNo || item.tradeOrderId === dto.tradeOrderId,
);
if (!order) {
throw new NotFoundException('Order not found');
}
if (order.status === 'paid') {
return {
orderNo: order.orderNo,
status: order.status,
unlocked: this.isUnlocked(order.userId, order.episodeId),
};
}
order.providerTransactionId = dto.providerTransactionId;
order.tiktokOrderId = dto.tiktokOrderId ?? order.tiktokOrderId;
order.status = dto.status;
order.updatedAt = new Date().toISOString();
if (dto.status === 'paid' && !this.isUnlocked(order.userId, order.episodeId)) {
this.unlocks.push({
id: this.unlockSequence++,
userId: order.userId,
episodeId: order.episodeId,
source: 'payment',
orderNo: order.orderNo,
createdAt: new Date().toISOString(),
});
}
return {
orderNo: order.orderNo,
tradeOrderId: order.tradeOrderId,
status: order.status,
unlocked: this.isUnlocked(order.userId, order.episodeId),
};
}
hasEpisodePermission(userId: number, episodeId: number) {
const episode = this.dramasService.getPublishedEpisodeOrThrow(episodeId);
return !episode.isPaid || this.isUnlocked(userId, episodeId);
}
listUnlockRecords(input: {
page: number;
pageSize: number;
}): PaginatedResponse<UserEpisodeUnlockRecord> {
const page = Math.max(input.page, 1);
const pageSize = Math.min(Math.max(input.pageSize, 1), 100);
const start = (page - 1) * pageSize;
const records = [...this.unlocks].reverse();
return {
list: records.slice(start, start + pageSize),
pagination: {
page,
pageSize,
total: records.length,
},
};
}
private isUnlocked(userId: number, episodeId: number) {
return this.unlocks.some(
(unlock) => unlock.userId === userId && unlock.episodeId === episodeId,
);
}
private createOrderNo() {
return `ORD${Date.now()}${String(this.orderSequence).padStart(6, '0')}`;
}
private createTradeOrderId() {
return `TRADE${Date.now()}${String(this.orderSequence).padStart(6, '0')}`;
}
}