refactor: 重构项目目录与代码结构,清理冗余文件

本次提交进行了大规模的项目结构调整:
1. 调整文件目录归属,将模块代码按功能域重新组织
2. 删除多处冗余的DTO、实体类和模块文件
3. 统一了常量、装饰器、工具类的存放位置
4. 修复了`.gitignore`的日志目录匹配规则
5. 新增了健康检查、认证、用户、剧集、订单等核心业务模块
This commit is contained in:
2026-07-02 19:17:43 +08:00
parent 55e20f9f16
commit e84351e8ed
99 changed files with 522 additions and 441 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Logs # Logs
logs /logs/
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

View File

@@ -0,0 +1,64 @@
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiMessage } from '../../common/decorators/api-message.decorator';
import { RequirePermissions } from '../admin/decorators/require-permissions.decorator';
import { AdminJwtAuthGuard } from '../admin/guards/admin-jwt-auth.guard';
import { PermissionsGuard } from '../admin/guards/permissions.guard';
import { CreatePlaybackErrorLogDto } from './dto/create-playback-error-log.dto';
import { LogsService } from './logs.service';
@ApiTags('Logs')
@Controller()
export class LogsController {
constructor(private readonly logsService: LogsService) {}
@Get('/api/admin/v1/logs/requests')
@ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('log:read')
listRequestLogs(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.logsService.listRequestLogs({
page: Number(page) || 1,
pageSize: Number(pageSize) || 20,
});
}
@Get('/api/admin/v1/logs/requests/:requestId')
@ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('log:read')
getRequestLog(@Param('requestId') requestId: string) {
return this.logsService.getRequestLogOrThrow(requestId);
}
@Post('/api/app/v1/logs/playback-errors')
@ApiMessage('success')
createPlaybackError(@Body() dto: CreatePlaybackErrorLogDto) {
return this.logsService.createPlaybackError(dto);
}
@Get('/api/admin/v1/logs/playback-errors')
@ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('log:read')
listPlaybackErrors(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.logsService.listPlaybackErrors({
page: Number(page) || 1,
pageSize: Number(pageSize) || 20,
});
}
@Get('/api/admin/v1/logs/playback-errors/:id')
@ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('log:read')
getPlaybackError(@Param('id') id: string) {
return this.logsService.getPlaybackErrorOrThrow(Number(id));
}
}

62
src/logs/logs.dto.ts Normal file
View File

@@ -0,0 +1,62 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsDateString,
IsInt,
IsNotEmpty,
IsObject,
IsOptional,
IsString,
IsUrl,
IsUUID,
Min,
} from 'class-validator';
export class CreatePlaybackErrorLogDto {
@ApiProperty()
@IsUUID(4)
requestId: string;
@ApiProperty()
@IsInt()
@Min(1)
dramaId: number;
@ApiProperty()
@IsInt()
@Min(1)
episodeId: number;
@ApiProperty()
@IsUrl({ require_tld: false })
playUrl: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
playerErrorCode: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
message: string;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(0)
progressSeconds?: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
networkType?: string;
@ApiPropertyOptional()
@IsOptional()
@IsObject()
device?: Record<string, unknown>;
@ApiProperty()
@IsDateString()
occurredAt: string;
}

12
src/logs/logs.module.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AdminModule } from '../admin/admin.module';
import { LogsController } from './logs.controller';
import { LogsService } from './logs.service';
@Module({
imports: [AdminModule],
controllers: [LogsController],
providers: [LogsService],
exports: [LogsService],
})
export class LogsModule {}

240
src/logs/logs.service.ts Normal file
View File

@@ -0,0 +1,240 @@
import { Injectable, NotFoundException, Optional } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
import { CreatePlaybackErrorLogDto } from './dto/create-playback-error-log.dto';
import { PlaybackErrorLog } from './entities/playback-error-log.entity';
import { RequestLog } from './entities/request-log.entity';
import {
PlaybackErrorLogRecord,
RequestLogRecord,
} from './interfaces/log-records.interface';
interface PaginationInput {
page: number;
pageSize: number;
}
@Injectable()
export class LogsService {
private requestLogSequence = 1;
private playbackErrorSequence = 1;
private readonly requestLogs = new Map<string, RequestLogRecord>();
private readonly playbackErrors: PlaybackErrorLogRecord[] = [];
constructor(
@Optional()
@InjectDataSource()
readonly dataSource?: DataSource,
) {}
async recordRequest(record: Omit<RequestLogRecord, 'id' | 'createdAt'>) {
if (!record.requestId) {
return;
}
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(RequestLog);
const existing = await repository.findOne({
where: { requestId: record.requestId },
});
await repository.save(
repository.create({
...existing,
...record,
}),
);
return;
}
this.requestLogs.set(record.requestId, {
...record,
id: this.requestLogs.get(record.requestId)?.id ?? this.requestLogSequence++,
createdAt:
this.requestLogs.get(record.requestId)?.createdAt ??
new Date().toISOString(),
});
}
async listRequestLogs(
pagination: PaginationInput,
): Promise<PaginatedResponse<RequestLogRecord>> {
if (this.hasDatabase()) {
const page = Math.max(pagination.page, 1);
const pageSize = Math.min(Math.max(pagination.pageSize, 1), 100);
const repository = this.dataSource.getRepository(RequestLog);
const [records, total] = await repository.findAndCount({
order: { id: 'DESC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
return {
list: records.map((record) => this.toRequestLogRecord(record)),
pagination: { page, pageSize, total },
};
}
const list = Array.from(this.requestLogs.values()).sort(
(left, right) => (right.id ?? 0) - (left.id ?? 0),
);
return this.paginate(list, pagination);
}
async getRequestLogOrThrow(requestId: string) {
if (this.hasDatabase()) {
const log = await this.dataSource
.getRepository(RequestLog)
.findOne({ where: { requestId } });
if (!log) {
throw new NotFoundException('Request log not found');
}
return this.toRequestLogRecord(log);
}
const log = this.requestLogs.get(requestId);
if (!log) {
throw new NotFoundException('Request log not found');
}
return log;
}
async createPlaybackError(dto: CreatePlaybackErrorLogDto) {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(PlaybackErrorLog);
const log = await repository.save(
repository.create({
...dto,
occurredAt: new Date(dto.occurredAt),
}),
);
return {
id: Number(log.id),
requestId: log.requestId,
};
}
const log: PlaybackErrorLogRecord = {
id: this.playbackErrorSequence++,
requestId: dto.requestId,
dramaId: dto.dramaId,
episodeId: dto.episodeId,
playUrl: dto.playUrl,
playerErrorCode: dto.playerErrorCode,
message: dto.message,
progressSeconds: dto.progressSeconds,
networkType: dto.networkType,
device: dto.device,
occurredAt: dto.occurredAt,
createdAt: new Date().toISOString(),
};
this.playbackErrors.push(log);
return {
id: log.id,
requestId: log.requestId,
};
}
async listPlaybackErrors(
pagination: PaginationInput,
): Promise<PaginatedResponse<PlaybackErrorLogRecord>> {
if (this.hasDatabase()) {
const page = Math.max(pagination.page, 1);
const pageSize = Math.min(Math.max(pagination.pageSize, 1), 100);
const repository = this.dataSource.getRepository(PlaybackErrorLog);
const [records, total] = await repository.findAndCount({
order: { id: 'DESC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
return {
list: records.map((record) => this.toPlaybackErrorLogRecord(record)),
pagination: { page, pageSize, total },
};
}
return this.paginate([...this.playbackErrors].reverse(), pagination);
}
async getPlaybackErrorOrThrow(id: number) {
if (this.hasDatabase()) {
const log = await this.dataSource
.getRepository(PlaybackErrorLog)
.findOne({ where: { id } });
if (!log) {
throw new NotFoundException('Playback error log not found');
}
return this.toPlaybackErrorLogRecord(log);
}
const log = this.playbackErrors.find((item) => item.id === id);
if (!log) {
throw new NotFoundException('Playback error log not found');
}
return log;
}
private hasDatabase(): this is this & { dataSource: DataSource } {
return Boolean(this.dataSource?.isInitialized);
}
private toRequestLogRecord(log: RequestLog): RequestLogRecord {
return {
id: Number(log.id),
requestId: log.requestId,
method: log.method,
path: log.path,
query: log.query,
body: log.body,
statusCode: log.statusCode,
responseCode: log.responseCode,
message: log.message,
costMs: log.costMs,
userId: log.userId ? Number(log.userId) : undefined,
adminId: log.adminId ? Number(log.adminId) : undefined,
ip: log.ip,
userAgent: log.userAgent,
errorStack: log.errorStack,
createdAt: log.createdAt?.toISOString?.(),
};
}
private toPlaybackErrorLogRecord(log: PlaybackErrorLog): PlaybackErrorLogRecord {
return {
id: Number(log.id),
requestId: log.requestId,
userId: log.userId ? Number(log.userId) : undefined,
dramaId: Number(log.dramaId),
episodeId: Number(log.episodeId),
playUrl: log.playUrl,
playerErrorCode: log.playerErrorCode,
message: log.message,
progressSeconds: log.progressSeconds,
networkType: log.networkType,
device: log.device,
occurredAt: log.occurredAt.toISOString(),
createdAt: log.createdAt?.toISOString?.() ?? new Date().toISOString(),
};
}
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,
},
};
}
}

34
src/logs/logs.types.ts Normal file
View File

@@ -0,0 +1,34 @@
export interface RequestLogRecord {
id?: number;
requestId: string;
method: string;
path: string;
query?: unknown;
body?: unknown;
statusCode: number;
responseCode: number;
message: string;
costMs: number;
userId?: number;
adminId?: number;
ip?: string;
userAgent?: string;
errorStack?: string;
createdAt?: string;
}
export interface PlaybackErrorLogRecord {
id: number;
requestId: string;
userId?: number;
dramaId: number;
episodeId: number;
playUrl: string;
playerErrorCode: string;
message: string;
progressSeconds?: number;
networkType?: string;
device?: Record<string, unknown>;
occurredAt: string;
createdAt: string;
}

View File

@@ -0,0 +1,50 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity('playback_error_logs')
export class PlaybackErrorLog {
@PrimaryGeneratedColumn()
id: number;
@Index()
@Column({ type: 'char', length: 36 })
requestId: string;
@Column({ type: 'bigint', nullable: true })
userId?: number;
@Column({ type: 'bigint' })
dramaId: number;
@Column({ type: 'bigint' })
episodeId: number;
@Column({ type: 'varchar', length: 1024 })
playUrl: string;
@Column({ type: 'varchar', length: 128 })
playerErrorCode: string;
@Column({ type: 'varchar', length: 1024 })
message: string;
@Column({ type: 'int', nullable: true })
progressSeconds?: number;
@Column({ type: 'varchar', length: 64, nullable: true })
networkType?: string;
@Column({ type: 'json', nullable: true })
device?: Record<string, unknown>;
@Column({ type: 'datetime' })
occurredAt: Date;
@CreateDateColumn()
createdAt: Date;
}

View File

@@ -0,0 +1,59 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity('request_logs')
export class RequestLog {
@PrimaryGeneratedColumn()
id: number;
@Index({ unique: true })
@Column({ type: 'char', length: 36 })
requestId: string;
@Column({ type: 'varchar', length: 16 })
method: string;
@Column({ type: 'varchar', length: 1024 })
path: string;
@Column({ type: 'json', nullable: true })
query?: unknown;
@Column({ type: 'json', nullable: true })
body?: unknown;
@Column({ type: 'int' })
statusCode: number;
@Column({ type: 'int' })
responseCode: number;
@Column({ type: 'varchar', length: 512 })
message: string;
@Column({ type: 'int' })
costMs: number;
@Column({ type: 'bigint', nullable: true })
userId?: number;
@Column({ type: 'bigint', nullable: true })
adminId?: number;
@Column({ type: 'varchar', length: 128, nullable: true })
ip?: string;
@Column({ type: 'varchar', length: 512, nullable: true })
userAgent?: string;
@Column({ type: 'text', nullable: true })
errorStack?: string;
@CreateDateColumn()
createdAt: Date;
}

View File

@@ -1,13 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';
export class ChangeAdminPasswordDto {
@ApiProperty()
@IsString()
oldPassword: string;
@ApiProperty()
@IsString()
@MinLength(6)
newPassword: string;
}

View File

@@ -1,38 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsArray,
IsEmail,
IsInt,
IsOptional,
IsString,
Min,
MinLength,
} from 'class-validator';
export class CreateAdminUserDto {
@ApiProperty()
@IsString()
username: string;
@ApiProperty()
@IsString()
@MinLength(6)
password: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
nickname?: string;
@ApiPropertyOptional()
@IsOptional()
@IsEmail()
email?: string;
@ApiPropertyOptional({ type: [Number] })
@IsOptional()
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
roleIds?: number[];
}

View File

@@ -1,17 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class CreatePermissionDto {
@ApiProperty()
@IsString()
code: string;
@ApiProperty()
@IsString()
name: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
}

View File

@@ -1,30 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsArray,
IsInt,
IsOptional,
IsString,
Min,
} from 'class-validator';
export class CreateRoleDto {
@ApiProperty()
@IsString()
code: string;
@ApiProperty()
@IsString()
name: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiPropertyOptional({ type: [Number] })
@IsOptional()
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
permissionIds?: number[];
}

View File

@@ -1,14 +0,0 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsEmail, IsOptional, IsString } from 'class-validator';
export class UpdateAdminProfileDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
nickname?: string;
@ApiPropertyOptional()
@IsOptional()
@IsEmail()
email?: string;
}

View File

@@ -1,10 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsInt, Min } from 'class-validator';
export class UpdateAdminRolesDto {
@ApiProperty({ type: [Number] })
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
roleIds: number[];
}

View File

@@ -1,8 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn } from 'class-validator';
export class UpdateAdminStatusDto {
@ApiProperty({ enum: ['active', 'disabled'] })
@IsIn(['active', 'disabled'])
status: 'active' | 'disabled';
}

View File

@@ -1,10 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsInt, Min } from 'class-validator';
export class UpdateRolePermissionsDto {
@ApiProperty({ type: [Number] })
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
permissionIds: number[];
}

View File

@@ -1,55 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminUser } from '../admin/entities/admin-user.entity';
import { Permission } from '../admin/entities/permission.entity';
import { Role } from '../admin/entities/role.entity';
import { DataSyncJob } from '../data/entities/data-sync-job.entity';
import { DramaMetric } from '../data/entities/drama-metric.entity';
import { EpisodeMetric } from '../data/entities/episode-metric.entity';
import { Drama } from '../dramas/entities/drama.entity';
import { Episode } from '../dramas/entities/episode.entity';
import { PlaybackErrorLog } from '../logs/entities/playback-error-log.entity';
import { RequestLog } from '../logs/entities/request-log.entity';
import { Cover } from '../media/entities/cover.entity';
import { MediaUploadJob } from '../media/entities/media-upload-job.entity';
import { Order } from '../orders/entities/order.entity';
import { UserEpisodeUnlock } from '../orders/entities/user-episode-unlock.entity';
import { UserPlaybackHistory } from '../history/entities/user-playback-history.entity';
import { User } from '../users/entities/user.entity';
const databaseImports = process.env.DB_HOST
? [
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT) || 3306,
username: process.env.DB_USERNAME ?? 'root',
password: process.env.DB_PASSWORD ?? '',
database: process.env.DB_DATABASE ?? 'cth_tk',
entities: [
AdminUser,
Permission,
Role,
DataSyncJob,
DramaMetric,
EpisodeMetric,
Drama,
Episode,
PlaybackErrorLog,
RequestLog,
Cover,
MediaUploadJob,
Order,
UserEpisodeUnlock,
UserPlaybackHistory,
User,
],
synchronize: process.env.DB_SYNCHRONIZE === 'true',
}),
]
: [];
@Module({
imports: databaseImports,
})
export class DatabaseModule {}

View File

@@ -1,10 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, Max, Min } from 'class-validator';
export class ConfigureEpisodesDto {
@ApiProperty()
@IsInt()
@Min(1)
@Max(500)
episodeCount: number;
}

View File

@@ -1,145 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsBoolean,
IsDateString,
IsEnum,
IsInt,
IsOptional,
IsString,
IsUrl,
Min,
} from 'class-validator';
import { PublishStatus } from '../drama-status.enum';
export class CreateEpisodeDto {
@ApiProperty()
@IsOptional()
@IsInt()
@Min(1)
dramaId?: number;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(1)
albumId?: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
tiktokEpisodeId?: string;
@ApiProperty()
@IsOptional()
@IsInt()
@Min(1)
episodeNo?: number;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(1)
seq?: number;
@ApiProperty()
@IsString()
title: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiProperty()
@IsOptional()
@IsUrl({ require_tld: false })
coverUrl: string;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(1)
coverId?: number;
@ApiProperty()
@IsOptional()
@IsUrl({ require_tld: false })
videoUrl?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
byteplusVid?: string;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(0)
trialDurationSeconds?: number;
@ApiProperty()
@IsInt()
@Min(1)
durationSeconds: number;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(1)
width?: number;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(1)
height?: number;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
isPaid?: boolean;
@ApiPropertyOptional({ enum: ['free', 'paid'] })
@IsOptional()
@IsString()
freeType?: 'free' | 'paid';
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(0)
unlockPrice?: number;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(0)
price?: number;
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
publishAt?: string;
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
nextReleaseAt?: string;
@ApiPropertyOptional({ enum: PublishStatus })
@IsOptional()
@IsEnum(PublishStatus)
status?: PublishStatus;
@ApiPropertyOptional({
enum: ['not_submitted', 'pending', 'reviewing', 'approved', 'rejected'],
})
@IsOptional()
@IsString()
reviewStatus?: 'not_submitted' | 'pending' | 'reviewing' | 'approved' | 'rejected';
@ApiPropertyOptional({ enum: PublishStatus })
@IsOptional()
@IsEnum(PublishStatus)
publishStatus?: PublishStatus;
}

View File

@@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateDramaDto } from './create-drama.dto';
export class UpdateDramaDto extends PartialType(CreateDramaDto) {}

View File

@@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateEpisodeDto } from './create-episode.dto';
export class UpdateEpisodeDto extends PartialType(CreateEpisodeDto) {}

View File

@@ -1,7 +0,0 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View File

@@ -1,9 +0,0 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class CompleteDirectUploadTaskDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
providerUploadId?: string;
}

View File

@@ -1,12 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsUrl } from 'class-validator';
export class CreateCoverDto {
@ApiProperty()
@IsString()
fileName: string;
@ApiProperty()
@IsUrl({ require_tld: false })
url: string;
}

View File

@@ -1,22 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsString, Min } from 'class-validator';
export class CreateDirectUploadTaskDto {
@ApiProperty()
@IsInt()
@Min(1)
episodeId: number;
@ApiProperty()
@IsString()
fileName: string;
@ApiProperty()
@IsString()
contentType: string;
@ApiProperty()
@IsInt()
@Min(1)
fileSize: number;
}

View File

@@ -1,32 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsIn, IsOptional, IsString } from 'class-validator';
export class TikTokPaymentWebhookDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
orderNo?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
tradeOrderId?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
tiktokOrderId?: string;
@ApiProperty()
@IsString()
providerTransactionId: string;
@ApiProperty({ enum: ['paid', 'failed'] })
@IsIn(['paid', 'failed'])
status: 'paid' | 'failed';
@ApiPropertyOptional()
@IsOptional()
@IsString()
signature?: string;
}