chore: 初始化TikTok短剧后端项目基础架构
创建了完整的NestJS项目结构,包含配置文件、核心模块、工具类、拦截器、过滤器等基础代码,实现了健康检查、用户认证、管理员RBAC、剧集管理、订单支付、媒体管理等核心功能的基础框架,配置了ESLint、TypeScript、Jest等开发工具链。
This commit is contained in:
147
src/modules/orders/orders.service.ts
Normal file
147
src/modules/orders/orders.service.ts
Normal 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')}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user