refactor: 重构项目目录与代码结构,清理冗余文件
本次提交进行了大规模的项目结构调整: 1. 调整文件目录归属,将模块代码按功能域重新组织 2. 删除多处冗余的DTO、实体类和模块文件 3. 统一了常量、装饰器、工具类的存放位置 4. 修复了`.gitignore`的日志目录匹配规则 5. 新增了健康检查、认证、用户、剧集、订单等核心业务模块
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
/logs/
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|||||||
64
src/logs/logs.controller.ts
Normal file
64
src/logs/logs.controller.ts
Normal 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
62
src/logs/logs.dto.ts
Normal 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
12
src/logs/logs.module.ts
Normal 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
240
src/logs/logs.service.ts
Normal 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
34
src/logs/logs.types.ts
Normal 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;
|
||||||
|
}
|
||||||
50
src/logs/playback-error-log.entity.ts
Normal file
50
src/logs/playback-error-log.entity.ts
Normal 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;
|
||||||
|
}
|
||||||
59
src/logs/request-log.entity.ts
Normal file
59
src/logs/request-log.entity.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
|
||||||
import { CreateDramaDto } from './create-drama.dto';
|
|
||||||
|
|
||||||
export class UpdateDramaDto extends PartialType(CreateDramaDto) {}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
|
||||||
import { CreateEpisodeDto } from './create-episode.dto';
|
|
||||||
|
|
||||||
export class UpdateEpisodeDto extends PartialType(CreateEpisodeDto) {}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { HealthController } from './health.controller';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [HealthController],
|
|
||||||
})
|
|
||||||
export class HealthModule {}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
||||||
import { IsOptional, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class CompleteDirectUploadTaskDto {
|
|
||||||
@ApiPropertyOptional()
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
providerUploadId?: string;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user