refactor: 完成项目整体目录重构与路径别名配置
1. 重构项目文件结构,统一文件存放位置与命名规则 2. 配置tsconfig路径别名,替换原有相对路径导入 3. 迁移drama-status枚举到dramas.types统一管理 4. 新增全局配置文件与工具类,补充完整注释与文档 5. 修复所有导入路径与依赖引用问题
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Paginated response dto.
|
||||
* @file 通用分页响应结构
|
||||
* @module common/paginated-response.dto
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
export interface PaginationMeta {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
|
||||
59
config/app.config.ts
Normal file
59
config/app.config.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* App config.
|
||||
* @file 应用运行配置
|
||||
* @module config/app.config
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
// 注意:本文件不主动加载 .env,环境变量由 src/main.ts 顶部的 `import "dotenv/config"` 注入。
|
||||
// e2e 测试不加载 .env,DB_CONFIG.host 为空时应用自动退回内存模式(见 src/app/app.module.ts 的门控)。
|
||||
|
||||
const numberEnv = (key: string, fallback: number) => {
|
||||
const value = Number(process.env[key]);
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
};
|
||||
|
||||
const boolEnv = (key: string, fallback: boolean) => {
|
||||
const value = process.env[key];
|
||||
if (value === undefined) return fallback;
|
||||
return value === "true" || value === "1";
|
||||
};
|
||||
|
||||
export const APP_CONFIG = {
|
||||
APP_NAME: "cth-tk-backend",
|
||||
PORT: numberEnv("PORT", 3000),
|
||||
};
|
||||
|
||||
export const OPTIONS_CONFIG = {
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN?.split(","),
|
||||
credentials: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const DB_CONFIG = {
|
||||
type: "mysql" as const,
|
||||
host: process.env.DB_HOST,
|
||||
port: numberEnv("DB_PORT", 3306),
|
||||
username: process.env.DB_USERNAME ?? "root",
|
||||
password: process.env.DB_PASSWORD ?? "",
|
||||
database: process.env.DB_DATABASE ?? "cth_tk",
|
||||
synchronize: boolEnv("DB_SYNCHRONIZE", false),
|
||||
};
|
||||
|
||||
export const AUTH_CONFIG = {
|
||||
jwtTokenSecret: process.env.JWT_SECRET ?? "development-secret",
|
||||
expiresIn: "30d" as const,
|
||||
};
|
||||
|
||||
export const ADMIN_AUTH_CONFIG = {
|
||||
jwtTokenSecret: process.env.ADMIN_JWT_SECRET ?? process.env.JWT_SECRET ?? "development-secret",
|
||||
expiresIn: "12h" as const,
|
||||
username: process.env.ADMIN_USERNAME ?? "admin",
|
||||
password: process.env.ADMIN_PASSWORD ?? "admin123",
|
||||
email: process.env.ADMIN_EMAIL ?? "admin@example.com",
|
||||
};
|
||||
|
||||
export const BYTEPLUS_CONFIG = {
|
||||
bucket: process.env.BYTEPLUS_BUCKET ?? "mock-short-drama",
|
||||
};
|
||||
@@ -1 +1,8 @@
|
||||
export const SUCCESS_MESSAGE = 'success';
|
||||
/**
|
||||
* Text constant.
|
||||
* @file 通用文案常量
|
||||
* @module constants/text.constant
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
export const SUCCESS_MESSAGE = "success";
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
/**
|
||||
* ApiMessage decorator.
|
||||
* @file 接口响应文案装饰器
|
||||
* @module decorators/api-message.decorator
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
export const API_MESSAGE_METADATA = 'api:message';
|
||||
import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
export const ApiMessage = (message: string) =>
|
||||
SetMetadata(API_MESSAGE_METADATA, message);
|
||||
export const API_MESSAGE_METADATA = "api:message";
|
||||
|
||||
export const ApiMessage = (message: string) => SetMetadata(API_MESSAGE_METADATA, message);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { AdminUserRecord } from '../interfaces/admin-records.interface';
|
||||
/**
|
||||
* CurrentAdmin decorator.
|
||||
* @file 当前登录管理员装饰器
|
||||
* @module decorators/current-admin.decorator
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
export const CurrentAdmin = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext): AdminUserRecord | undefined => {
|
||||
const request = ctx
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { adminUser?: AdminUserRecord }>();
|
||||
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
import { AdminUserRecord } from "@src/admin/admin.types";
|
||||
|
||||
export const CurrentAdmin = createParamDecorator((_data: unknown, ctx: ExecutionContext): AdminUserRecord | undefined => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { adminUser?: AdminUserRecord }>();
|
||||
return request.adminUser;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { UserRecord } from '../../users/interfaces/user-record.interface';
|
||||
/**
|
||||
* CurrentUser decorator.
|
||||
* @file 当前登录用户装饰器
|
||||
* @module decorators/current-user.decorator
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext): UserRecord | undefined => {
|
||||
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
import { UserRecord } from "@src/users/users.types";
|
||||
|
||||
export const CurrentUser = createParamDecorator((_data: unknown, ctx: ExecutionContext): UserRecord | undefined => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { user?: UserRecord }>();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
/**
|
||||
* RequirePermissions decorator.
|
||||
* @file 接口权限声明装饰器
|
||||
* @module decorators/require-permissions.decorator
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
export const REQUIRED_PERMISSIONS_METADATA = 'admin:required-permissions';
|
||||
import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
export const RequirePermissions = (...permissions: string[]) =>
|
||||
SetMetadata(REQUIRED_PERMISSIONS_METADATA, permissions);
|
||||
export const REQUIRED_PERMISSIONS_METADATA = "admin:required-permissions";
|
||||
|
||||
export const RequirePermissions = (...permissions: string[]) => SetMetadata(REQUIRED_PERMISSIONS_METADATA, permissions);
|
||||
|
||||
@@ -5,7 +5,22 @@ export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ['src/**/*.ts', 'test/**/*.ts'],
|
||||
ignores: ['dist/**', 'node_modules/**', '.pnpm-store/**'],
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'src/**/*.ts',
|
||||
'test/**/*.ts',
|
||||
'config/**/*.ts',
|
||||
'constants/**/*.ts',
|
||||
'decorators/**/*.ts',
|
||||
'filters/**/*.ts',
|
||||
'guards/**/*.ts',
|
||||
'interceptor/**/*.ts',
|
||||
'middleware/**/*.ts',
|
||||
'utils/**/*.ts',
|
||||
'common/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { LogsService } from '../../modules/logs/logs.service';
|
||||
import { getRequestCost } from '../utils/request-context.util';
|
||||
/**
|
||||
* HttpException filter.
|
||||
* @file 全局异常过滤器
|
||||
* @module filters/http.filter
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
|
||||
import { Request, Response } from "express";
|
||||
import { LogsService } from "@src/logs/logs.service";
|
||||
import { getRequestCost } from "@app/utils/request-context.util";
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
@@ -15,16 +16,11 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const request = ctx.getRequest<
|
||||
Request & { requestId?: string; adminId?: number; user?: { id: number } }
|
||||
>();
|
||||
const request = ctx.getRequest<Request & { requestId?: string; adminId?: number; user?: { id: number } }>();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const statusCode =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
const statusCode = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
const message = this.getMessage(exception, statusCode);
|
||||
const requestId = request.requestId ?? '';
|
||||
const requestId = request.requestId ?? "";
|
||||
const costMs = getRequestCost(request);
|
||||
|
||||
this.logsService.recordRequest({
|
||||
@@ -40,7 +36,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
userId: request.user?.id,
|
||||
adminId: request.adminId,
|
||||
ip: request.ip,
|
||||
userAgent: request.get('user-agent'),
|
||||
userAgent: request.get("user-agent"),
|
||||
errorStack: exception instanceof Error ? exception.stack : undefined,
|
||||
});
|
||||
|
||||
@@ -57,23 +53,19 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private getMessage(exception: unknown, statusCode: number): string {
|
||||
if (exception instanceof HttpException) {
|
||||
const response = exception.getResponse();
|
||||
if (typeof response === 'string') {
|
||||
if (typeof response === "string") {
|
||||
return response;
|
||||
}
|
||||
if (
|
||||
response &&
|
||||
typeof response === 'object' &&
|
||||
'message' in response
|
||||
) {
|
||||
if (response && typeof response === "object" && "message" in response) {
|
||||
const message = (response as { message: string | string[] }).message;
|
||||
return Array.isArray(message) ? message.join('; ') : message;
|
||||
return Array.isArray(message) ? message.join("; ") : message;
|
||||
}
|
||||
}
|
||||
|
||||
if (exception instanceof Error && statusCode === HttpStatus.INTERNAL_SERVER_ERROR) {
|
||||
return exception.message || 'Internal server error';
|
||||
return exception.message || "Internal server error";
|
||||
}
|
||||
|
||||
return 'Internal server error';
|
||||
return "Internal server error";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { AdminUserRecord } from '../interfaces/admin-records.interface';
|
||||
import { AdminService } from '../admin.service';
|
||||
/**
|
||||
* AdminJwtAuth guard.
|
||||
* @file 管理端 JWT 认证守卫
|
||||
* @module guards/admin-jwt-auth.guard
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Request } from "express";
|
||||
import { AdminUserRecord } from "@src/admin/admin.types";
|
||||
import { AdminService } from "@src/admin/admin.service";
|
||||
|
||||
@Injectable()
|
||||
export class AdminJwtAuthGuard implements CanActivate {
|
||||
@@ -17,16 +19,12 @@ export class AdminJwtAuthGuard implements CanActivate {
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { adminUser?: AdminUserRecord; adminId?: number }>();
|
||||
const authHeader = request.get('authorization');
|
||||
const token = authHeader?.startsWith('Bearer ')
|
||||
? authHeader.slice('Bearer '.length)
|
||||
: undefined;
|
||||
const request = context.switchToHttp().getRequest<Request & { adminUser?: AdminUserRecord; adminId?: number }>();
|
||||
const authHeader = request.get("authorization");
|
||||
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : undefined;
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('Admin not authenticated');
|
||||
throw new UnauthorizedException("Admin not authenticated");
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -34,20 +32,20 @@ export class AdminJwtAuthGuard implements CanActivate {
|
||||
sub: number;
|
||||
type: string;
|
||||
}>(token);
|
||||
if (payload.type !== 'admin') {
|
||||
throw new UnauthorizedException('Admin not authenticated');
|
||||
if (payload.type !== "admin") {
|
||||
throw new UnauthorizedException("Admin not authenticated");
|
||||
}
|
||||
|
||||
const adminUser = await this.adminService.findAdminById(payload.sub);
|
||||
if (!adminUser || adminUser.status !== 'active') {
|
||||
throw new UnauthorizedException('Admin not authenticated');
|
||||
if (!adminUser || adminUser.status !== "active") {
|
||||
throw new UnauthorizedException("Admin not authenticated");
|
||||
}
|
||||
|
||||
request.adminUser = adminUser;
|
||||
request.adminId = adminUser.id;
|
||||
return true;
|
||||
} catch {
|
||||
throw new UnauthorizedException('Admin not authenticated');
|
||||
throw new UnauthorizedException("Admin not authenticated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { UsersService } from '../../users/users.service';
|
||||
/**
|
||||
* JwtAuth guard.
|
||||
* @file 用户端 JWT 认证守卫
|
||||
* @module guards/jwt-auth.guard
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Request } from "express";
|
||||
import { UsersService } from "@src/users/users.service";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
@@ -17,26 +19,24 @@ export class JwtAuthGuard implements CanActivate {
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { user?: unknown }>();
|
||||
const authHeader = request.get('authorization');
|
||||
const token = authHeader?.startsWith('Bearer ')
|
||||
? authHeader.slice('Bearer '.length)
|
||||
: undefined;
|
||||
const authHeader = request.get("authorization");
|
||||
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : undefined;
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('Not authenticated');
|
||||
throw new UnauthorizedException("Not authenticated");
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync<{ sub: number }>(token);
|
||||
const user = await this.usersService.findById(payload.sub);
|
||||
if (!user || user.status !== 'active') {
|
||||
throw new UnauthorizedException('Not authenticated');
|
||||
if (!user || user.status !== "active") {
|
||||
throw new UnauthorizedException("Not authenticated");
|
||||
}
|
||||
|
||||
request.user = user;
|
||||
return true;
|
||||
} catch {
|
||||
throw new UnauthorizedException('Not authenticated');
|
||||
throw new UnauthorizedException("Not authenticated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request } from 'express';
|
||||
import {
|
||||
REQUIRED_PERMISSIONS_METADATA,
|
||||
} from '../decorators/require-permissions.decorator';
|
||||
import { AdminUserRecord } from '../interfaces/admin-records.interface';
|
||||
import { AdminService } from '../admin.service';
|
||||
/**
|
||||
* Permissions guard.
|
||||
* @file 管理端权限校验守卫
|
||||
* @module guards/permissions.guard
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { Request } from "express";
|
||||
import { REQUIRED_PERMISSIONS_METADATA } from "@app/decorators/require-permissions.decorator";
|
||||
import { AdminUserRecord } from "@src/admin/admin.types";
|
||||
import { AdminService } from "@src/admin/admin.service";
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
@@ -15,38 +20,28 @@ export class PermissionsGuard implements CanActivate {
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const required =
|
||||
this.reflector.getAllAndOverride<string[]>(
|
||||
REQUIRED_PERMISSIONS_METADATA,
|
||||
[context.getHandler(), context.getClass()],
|
||||
) ?? [];
|
||||
const required = this.reflector.getAllAndOverride<string[]>(REQUIRED_PERMISSIONS_METADATA, [context.getHandler(), context.getClass()]) ?? [];
|
||||
|
||||
if (required.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { adminUser?: AdminUserRecord }>();
|
||||
const request = context.switchToHttp().getRequest<Request & { adminUser?: AdminUserRecord }>();
|
||||
const adminUser = request.adminUser;
|
||||
|
||||
if (!adminUser) {
|
||||
throw new ForbiddenException('No permission');
|
||||
throw new ForbiddenException("No permission");
|
||||
}
|
||||
|
||||
if (adminUser.isSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissions = await this.adminService.getPermissionCodesForAdmin(
|
||||
adminUser.id,
|
||||
);
|
||||
const allowed = required.every((permission) =>
|
||||
permissions.includes(permission),
|
||||
);
|
||||
const permissions = await this.adminService.getPermissionCodesForAdmin(adminUser.id);
|
||||
const allowed = required.every((permission) => permissions.includes(permission));
|
||||
|
||||
if (!allowed) {
|
||||
throw new ForbiddenException('No permission');
|
||||
throw new ForbiddenException("No permission");
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request, Response } from 'express';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import {
|
||||
API_MESSAGE_METADATA,
|
||||
} from '../decorators/api-message.decorator';
|
||||
import { SUCCESS_MESSAGE } from '../constants/response-message.constants';
|
||||
import { getRequestCost } from '../utils/request-context.util';
|
||||
import { LogsService } from '../../modules/logs/logs.service';
|
||||
/**
|
||||
* Logger interceptor.
|
||||
* @file 请求日志拦截器
|
||||
* @module interceptor/logger.interceptor
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { Request, Response } from "express";
|
||||
import { Observable, tap } from "rxjs";
|
||||
import { API_MESSAGE_METADATA } from "@app/decorators/api-message.decorator";
|
||||
import { SUCCESS_MESSAGE } from "@constants/text.constant";
|
||||
import { getRequestCost } from "@app/utils/request-context.util";
|
||||
import { LogsService } from "@src/logs/logs.service";
|
||||
|
||||
@Injectable()
|
||||
export class RequestLoggingInterceptor implements NestInterceptor {
|
||||
export class LoggerInterceptor implements NestInterceptor {
|
||||
constructor(
|
||||
private readonly logsService: LogsService,
|
||||
private readonly reflector: Reflector,
|
||||
@@ -23,18 +23,14 @@ export class RequestLoggingInterceptor implements NestInterceptor {
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const http = context.switchToHttp();
|
||||
const request = http.getRequest<
|
||||
Request & { requestId?: string; adminId?: number; user?: { id: number } }
|
||||
>();
|
||||
const request = http.getRequest<Request & { requestId?: string; adminId?: number; user?: { id: number } }>();
|
||||
const response = http.getResponse<Response>();
|
||||
const message =
|
||||
this.reflector.get<string>(API_MESSAGE_METADATA, context.getHandler()) ??
|
||||
SUCCESS_MESSAGE;
|
||||
const message = this.reflector.get<string>(API_MESSAGE_METADATA, context.getHandler()) ?? SUCCESS_MESSAGE;
|
||||
|
||||
return next.handle().pipe(
|
||||
tap(() => {
|
||||
this.logsService.recordRequest({
|
||||
requestId: request.requestId ?? '',
|
||||
requestId: request.requestId ?? "",
|
||||
method: request.method,
|
||||
path: request.originalUrl ?? request.url,
|
||||
query: request.query,
|
||||
@@ -46,7 +42,7 @@ export class RequestLoggingInterceptor implements NestInterceptor {
|
||||
userId: request.user?.id,
|
||||
adminId: request.adminId,
|
||||
ip: request.ip,
|
||||
userAgent: request.get('user-agent'),
|
||||
userAgent: request.get("user-agent"),
|
||||
errorStack: undefined,
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request } from 'express';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { SUCCESS_MESSAGE } from '../constants/response-message.constants';
|
||||
import {
|
||||
API_MESSAGE_METADATA,
|
||||
} from '../decorators/api-message.decorator';
|
||||
import { getRequestCost } from '../utils/request-context.util';
|
||||
/**
|
||||
* Response interceptor.
|
||||
* @file 统一响应结构拦截器
|
||||
* @module interceptor/response.interceptor
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { Request } from "express";
|
||||
import { map, Observable } from "rxjs";
|
||||
import { SUCCESS_MESSAGE } from "@constants/text.constant";
|
||||
import { API_MESSAGE_METADATA } from "@app/decorators/api-message.decorator";
|
||||
import { getRequestCost } from "@app/utils/request-context.util";
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor implements NestInterceptor {
|
||||
constructor(private readonly reflector = new Reflector()) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { requestId?: string }>();
|
||||
const message =
|
||||
this.reflector.get<string>(API_MESSAGE_METADATA, context.getHandler()) ??
|
||||
SUCCESS_MESSAGE;
|
||||
const request = context.switchToHttp().getRequest<Request & { requestId?: string }>();
|
||||
const message = this.reflector.get<string>(API_MESSAGE_METADATA, context.getHandler()) ?? SUCCESS_MESSAGE;
|
||||
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { validate as validateUuid, version as uuidVersion, v4 as uuidv4 } from 'uuid';
|
||||
/**
|
||||
* RequestId middleware.
|
||||
* @file 请求链路 ID 中间件
|
||||
* @module middleware/request-id.middleware
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
export function RequestIdMiddleware(
|
||||
request: Request & { requestId?: string; startTime?: bigint },
|
||||
response: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
const incomingRequestId = request.get('x-request-id');
|
||||
const requestId =
|
||||
incomingRequestId &&
|
||||
validateUuid(incomingRequestId) &&
|
||||
uuidVersion(incomingRequestId) === 4
|
||||
? incomingRequestId
|
||||
: uuidv4();
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { validate as validateUuid, version as uuidVersion, v4 as uuidv4 } from "uuid";
|
||||
|
||||
export function RequestIdMiddleware(request: Request & { requestId?: string; startTime?: bigint }, response: Response, next: NextFunction) {
|
||||
const incomingRequestId = request.get("x-request-id");
|
||||
const requestId = incomingRequestId && validateUuid(incomingRequestId) && uuidVersion(incomingRequestId) === 4 ? incomingRequestId : uuidv4();
|
||||
|
||||
request.requestId = requestId;
|
||||
request.startTime = process.hrtime.bigint();
|
||||
response.setHeader('X-Request-Id', requestId);
|
||||
response.setHeader("X-Request-Id", requestId);
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"start": "nest start",
|
||||
"serve": "nest start --watch",
|
||||
"start:dev": "nest start --watch",
|
||||
"debug": "nest start --debug --watch",
|
||||
"prod": "TS_NODE_BASEURL=./dist node -r tsconfig-paths/register dist/src/main",
|
||||
"test": "jest",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"lint": "eslint \"{src,test}/**/*.ts\""
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/config": "^4.0.0",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
@@ -29,6 +31,7 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typeorm": "^0.3.0",
|
||||
"uuid": "^11.0.0"
|
||||
},
|
||||
@@ -52,7 +55,6 @@
|
||||
"ts-jest": "^29.0.0",
|
||||
"ts-loader": "^9.5.0",
|
||||
"ts-node": "^10.9.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.8.0",
|
||||
"typescript-eslint": "^8.0.0"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentAdmin } from './decorators/current-admin.decorator';
|
||||
import { RequirePermissions } from './decorators/require-permissions.decorator';
|
||||
import { AdminLoginDto } from './dto/admin-login.dto';
|
||||
import { ChangeAdminPasswordDto } from './dto/change-admin-password.dto';
|
||||
import { CreateAdminUserDto } from './dto/create-admin-user.dto';
|
||||
import { CreatePermissionDto } from './dto/create-permission.dto';
|
||||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto';
|
||||
import { UpdateAdminRolesDto } from './dto/update-admin-roles.dto';
|
||||
import { UpdateAdminStatusDto } from './dto/update-admin-status.dto';
|
||||
import { UpdateRolePermissionsDto } from './dto/update-role-permissions.dto';
|
||||
import { AdminJwtAuthGuard } from './guards/admin-jwt-auth.guard';
|
||||
import { PermissionsGuard } from './guards/permissions.guard';
|
||||
import { AdminUserRecord } from './interfaces/admin-records.interface';
|
||||
import { CurrentAdmin } from '@app/decorators/current-admin.decorator';
|
||||
import { RequirePermissions } from '@app/decorators/require-permissions.decorator';
|
||||
import { AdminLoginDto, ChangeAdminPasswordDto, CreateAdminUserDto, CreatePermissionDto, CreateRoleDto, UpdateAdminProfileDto, UpdateAdminRolesDto, UpdateAdminStatusDto, UpdateRolePermissionsDto } from './admin.dto';
|
||||
import { AdminJwtAuthGuard } from '@app/guards/admin-jwt-auth.guard';
|
||||
import { PermissionsGuard } from '@app/guards/permissions.guard';
|
||||
import { AdminUserRecord } from './admin.types';
|
||||
import { AdminService } from './admin.service';
|
||||
|
||||
@ApiTags('Admin')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, MinLength } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsArray, IsEmail, IsIn, IsInt, IsOptional, IsString, Min, MinLength } from "class-validator";
|
||||
|
||||
export class AdminLoginDto {
|
||||
@ApiProperty()
|
||||
@@ -11,3 +11,113 @@ export class AdminLoginDto {
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class ChangeAdminPasswordDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
oldPassword: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
export class CreatePermissionDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
code: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
export class UpdateAdminProfileDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export class UpdateAdminRolesDto {
|
||||
@ApiProperty({ type: [Number] })
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@Min(1, { each: true })
|
||||
roleIds: number[];
|
||||
}
|
||||
|
||||
export class UpdateAdminStatusDto {
|
||||
@ApiProperty({ enum: ["active", "disabled"] })
|
||||
@IsIn(["active", "disabled"])
|
||||
status: "active" | "disabled";
|
||||
}
|
||||
|
||||
export class UpdateRolePermissionsDto {
|
||||
@ApiProperty({ type: [Number] })
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@Min(1, { each: true })
|
||||
permissionIds: number[];
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminJwtAuthGuard } from './guards/admin-jwt-auth.guard';
|
||||
import { PermissionsGuard } from './guards/permissions.guard';
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { ADMIN_AUTH_CONFIG } from "@config/app.config";
|
||||
import { AdminController } from "./admin.controller";
|
||||
import { AdminService } from "./admin.service";
|
||||
import { AdminJwtAuthGuard } from "@app/guards/admin-jwt-auth.guard";
|
||||
import { PermissionsGuard } from "@app/guards/permissions.guard";
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.register({
|
||||
secret: process.env.ADMIN_JWT_SECRET ?? process.env.JWT_SECRET ?? 'development-secret',
|
||||
signOptions: { expiresIn: '12h' },
|
||||
secret: ADMIN_AUTH_CONFIG.jwtTokenSecret,
|
||||
signOptions: { expiresIn: ADMIN_AUTH_CONFIG.expiresIn },
|
||||
}),
|
||||
],
|
||||
controllers: [AdminController],
|
||||
|
||||
@@ -11,21 +11,18 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
||||
import { AdminLoginDto } from './dto/admin-login.dto';
|
||||
import { CreateAdminUserDto } from './dto/create-admin-user.dto';
|
||||
import { CreatePermissionDto } from './dto/create-permission.dto';
|
||||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto';
|
||||
import { AdminUser } from './entities/admin-user.entity';
|
||||
import { Permission } from './entities/permission.entity';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { ADMIN_AUTH_CONFIG } from '@config/app.config';
|
||||
import { PaginatedResponse } from '@app/common/paginated-response.dto';
|
||||
import { AdminLoginDto, CreateAdminUserDto, CreatePermissionDto, CreateRoleDto, UpdateAdminProfileDto } from './admin.dto';
|
||||
import { AdminUser } from './admin-user.entity';
|
||||
import { Permission } from './permission.entity';
|
||||
import { Role } from './role.entity';
|
||||
import {
|
||||
AdminUserRecord,
|
||||
AdminUserView,
|
||||
PermissionRecord,
|
||||
RoleRecord,
|
||||
} from './interfaces/admin-records.interface';
|
||||
} from './admin.types';
|
||||
|
||||
interface PaginationInput {
|
||||
page: number;
|
||||
@@ -434,10 +431,10 @@ export class AdminService implements OnModuleInit {
|
||||
const now = new Date().toISOString();
|
||||
this.admins.push({
|
||||
id: this.adminSequence++,
|
||||
username: process.env.ADMIN_USERNAME ?? 'admin',
|
||||
passwordHash: bcrypt.hashSync(process.env.ADMIN_PASSWORD ?? 'admin123', 10),
|
||||
username: ADMIN_AUTH_CONFIG.username,
|
||||
passwordHash: bcrypt.hashSync(ADMIN_AUTH_CONFIG.password, 10),
|
||||
nickname: 'Super Admin',
|
||||
email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
|
||||
email: ADMIN_AUTH_CONFIG.email,
|
||||
status: 'active',
|
||||
isSuperAdmin: true,
|
||||
roleIds: [],
|
||||
@@ -466,15 +463,15 @@ export class AdminService implements OnModuleInit {
|
||||
}
|
||||
|
||||
const adminRepository = dataSource.getRepository(AdminUser);
|
||||
const username = process.env.ADMIN_USERNAME ?? 'admin';
|
||||
const username = ADMIN_AUTH_CONFIG.username;
|
||||
const existingAdmin = await adminRepository.findOne({ where: { username } });
|
||||
if (!existingAdmin) {
|
||||
await adminRepository.save(
|
||||
adminRepository.create({
|
||||
username,
|
||||
passwordHash: bcrypt.hashSync(process.env.ADMIN_PASSWORD ?? 'admin123', 10),
|
||||
passwordHash: bcrypt.hashSync(ADMIN_AUTH_CONFIG.password, 10),
|
||||
nickname: 'Super Admin',
|
||||
email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
|
||||
email: ADMIN_AUTH_CONFIG.email,
|
||||
status: 'active',
|
||||
isSuperAdmin: true,
|
||||
roleIds: [],
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
/**
|
||||
* App controller.
|
||||
* @file App 根控制器(健康检查)
|
||||
* @module src/app/controller
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
@ApiTags('Health')
|
||||
@Controller('/api/app/v1/health')
|
||||
export class HealthController {
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
|
||||
@ApiTags("Health")
|
||||
@Controller("/api/app/v1/health")
|
||||
export class AppController {
|
||||
@Get()
|
||||
getHealth() {
|
||||
return { status: 'ok' };
|
||||
return { status: "ok" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,70 @@
|
||||
/**
|
||||
* App module.
|
||||
* @file App 主模块
|
||||
* @module src/app/module
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { AdminModule } from "./modules/admin/admin.module";
|
||||
import { AuthModule } from "./modules/auth/auth.module";
|
||||
import { DataModule } from "./modules/data/data.module";
|
||||
import { DatabaseModule } from "./modules/database/database.module";
|
||||
import { DramasModule } from "./modules/dramas/dramas.module";
|
||||
import { HealthModule } from "./modules/health/health.module";
|
||||
import { LogsModule } from "./modules/logs/logs.module";
|
||||
import { MediaModule } from "./modules/media/media.module";
|
||||
import { OrdersModule } from "./modules/orders/orders.module";
|
||||
import { PlayerModule } from "./modules/player/player.module";
|
||||
import { HistoryModule } from "./modules/history/history.module";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { DB_CONFIG } from "@config/app.config";
|
||||
import { AppController } from "@src/app/app.controller";
|
||||
|
||||
// 业务模块
|
||||
import { AdminModule } from "@src/admin/admin.module";
|
||||
import { AuthModule } from "@src/auth/auth.module";
|
||||
import { UsersModule } from "@src/users/users.module";
|
||||
import { DramasModule } from "@src/dramas/dramas.module";
|
||||
import { DataModule } from "@src/data/data.module";
|
||||
import { MediaModule } from "@src/media/media.module";
|
||||
import { OrdersModule } from "@src/orders/orders.module";
|
||||
import { PlayerModule } from "@src/player/player.module";
|
||||
import { HistoryModule } from "@src/history/history.module";
|
||||
import { LogsModule } from "@src/logs/logs.module";
|
||||
|
||||
// 数据库实体
|
||||
import { AdminUser } from "@src/admin/admin-user.entity";
|
||||
import { Permission } from "@src/admin/permission.entity";
|
||||
import { Role } from "@src/admin/role.entity";
|
||||
import { DataSyncJob } from "@src/data/data-sync-job.entity";
|
||||
import { DramaMetric } from "@src/data/drama-metric.entity";
|
||||
import { EpisodeMetric } from "@src/data/episode-metric.entity";
|
||||
import { Drama } from "@src/dramas/drama.entity";
|
||||
import { Episode } from "@src/dramas/episode.entity";
|
||||
import { PlaybackErrorLog } from "@src/logs/playback-error-log.entity";
|
||||
import { RequestLog } from "@src/logs/request-log.entity";
|
||||
import { Cover } from "@src/media/cover.entity";
|
||||
import { MediaUploadJob } from "@src/media/media-upload-job.entity";
|
||||
import { Order } from "@src/orders/order.entity";
|
||||
import { UserEpisodeUnlock } from "@src/orders/user-episode-unlock.entity";
|
||||
import { UserPlaybackHistory } from "@src/history/user-playback-history.entity";
|
||||
import { User } from "@src/users/user.entity";
|
||||
|
||||
// 未配置 DB_HOST 时(如 e2e 测试)退回内存模式,不装配 TypeORM
|
||||
const databaseImports = DB_CONFIG.host
|
||||
? [
|
||||
TypeOrmModule.forRoot({
|
||||
...DB_CONFIG,
|
||||
entities: [AdminUser, Permission, Role, DataSyncJob, DramaMetric, EpisodeMetric, Drama, Episode, PlaybackErrorLog, RequestLog, Cover, MediaUploadJob, Order, UserEpisodeUnlock, UserPlaybackHistory, User],
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
DatabaseModule,
|
||||
AdminModule,
|
||||
HealthModule,
|
||||
LogsModule,
|
||||
DramasModule,
|
||||
DataModule,
|
||||
MediaModule,
|
||||
AuthModule,
|
||||
OrdersModule,
|
||||
PlayerModule,
|
||||
HistoryModule,
|
||||
...databaseImports,
|
||||
|
||||
AdminModule, // 管理端(人员/角色/权限)
|
||||
LogsModule, // 请求与播放错误日志
|
||||
DramasModule, // 短剧与剧集
|
||||
DataModule, // 数据指标与同步
|
||||
MediaModule, // 媒体上传与封面
|
||||
AuthModule, // 用户端认证
|
||||
UsersModule, // 用户
|
||||
OrdersModule, // 订单与解锁
|
||||
PlayerModule, // 播放器
|
||||
HistoryModule, // 播放历史
|
||||
],
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service';
|
||||
import { TikTokLoginDto } from './dto/tiktok-login.dto';
|
||||
import { TikTokLoginDto } from './auth.dto';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('/api/app/v1/auth')
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { AUTH_CONFIG } from "@config/app.config";
|
||||
import { UsersModule } from "@src/users/users.module";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtAuthGuard } from "@app/guards/jwt-auth.guard";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET ?? 'development-secret',
|
||||
signOptions: { expiresIn: '30d' },
|
||||
secret: AUTH_CONFIG.jwtTokenSecret,
|
||||
signOptions: { expiresIn: AUTH_CONFIG.expiresIn },
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { TikTokLoginDto } from './dto/tiktok-login.dto';
|
||||
import { UsersService } from '@src/users/users.service';
|
||||
import { TikTokLoginDto } from './auth.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
/**
|
||||
* App bootstrap.
|
||||
* @file 应用全局装配(管道/过滤器/拦截器/文档),供 main 与 e2e 测试共用
|
||||
* @module src/bootstrap
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { INestApplication, ValidationPipe } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
|
||||
import { RequestLoggingInterceptor } from "./common/interceptors/request-logging.interceptor";
|
||||
import { ResponseInterceptor } from "./common/interceptors/response.interceptor";
|
||||
import { RequestIdMiddleware } from "./common/middleware/request-id.middleware";
|
||||
import { LogsService } from "./modules/logs/logs.service";
|
||||
import { HttpExceptionFilter } from "@app/filters/http.filter";
|
||||
import { LoggerInterceptor } from "@app/interceptor/logger.interceptor";
|
||||
import { ResponseInterceptor } from "@app/interceptor/response.interceptor";
|
||||
import { RequestIdMiddleware } from "@app/middleware/request-id.middleware";
|
||||
import { LogsService } from "@src/logs/logs.service";
|
||||
|
||||
export function bootstrapApp(app: INestApplication) {
|
||||
app.enableCors({
|
||||
origin: process.env.CORS_ORIGIN?.split(","),
|
||||
credentials: true,
|
||||
});
|
||||
app.use(RequestIdMiddleware);
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
@@ -24,7 +27,7 @@ export function bootstrapApp(app: INestApplication) {
|
||||
const logsService = app.get(LogsService);
|
||||
|
||||
app.useGlobalFilters(new HttpExceptionFilter(logsService));
|
||||
app.useGlobalInterceptors(new RequestLoggingInterceptor(logsService, app.get(Reflector)), new ResponseInterceptor());
|
||||
app.useGlobalInterceptors(new LoggerInterceptor(logsService, app.get(Reflector)), new ResponseInterceptor());
|
||||
|
||||
const config = new DocumentBuilder().setTitle("TikTok Short Drama Backend").setDescription("RESTful API for TikTok short drama backend services.").setVersion("1.0").addBearerAuth().build();
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
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 { RequirePermissions } from '@app/decorators/require-permissions.decorator';
|
||||
import { AdminJwtAuthGuard } from '@app/guards/admin-jwt-auth.guard';
|
||||
import { PermissionsGuard } from '@app/guards/permissions.guard';
|
||||
import { DataService } from './data.service';
|
||||
|
||||
@ApiTags('Data')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { DramasModule } from '../dramas/dramas.module';
|
||||
import { AdminModule } from '@src/admin/admin.module';
|
||||
import { DramasModule } from '@src/dramas/dramas.module';
|
||||
import { DataController } from './data.controller';
|
||||
import { DataService } from './data.service';
|
||||
import { TikTokDataProvider } from './providers/tiktok-data.provider';
|
||||
import { TikTokDataProvider } from './data.provider';
|
||||
|
||||
@Module({
|
||||
imports: [AdminModule, DramasModule],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DramaRecord, EpisodeRecord } from '../../dramas/interfaces/drama-records.interface';
|
||||
import { DramaRecord, EpisodeRecord } from '@src/dramas/dramas.types';
|
||||
|
||||
@Injectable()
|
||||
export class TikTokDataProvider {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Injectable, Optional } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
||||
import { DramasService } from '../dramas/dramas.service';
|
||||
import { DataSyncJob } from './entities/data-sync-job.entity';
|
||||
import { DramaMetric } from './entities/drama-metric.entity';
|
||||
import { EpisodeMetric } from './entities/episode-metric.entity';
|
||||
import { TikTokDataProvider } from './providers/tiktok-data.provider';
|
||||
import { PaginatedResponse } from '@app/common/paginated-response.dto';
|
||||
import { DramasService } from '@src/dramas/dramas.service';
|
||||
import { DataSyncJob } from './data-sync-job.entity';
|
||||
import { DramaMetric } from './drama-metric.entity';
|
||||
import { EpisodeMetric } from './episode-metric.entity';
|
||||
import { TikTokDataProvider } from './data.provider';
|
||||
|
||||
interface PaginationInput {
|
||||
page: number;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum PublishStatus {
|
||||
Draft = 'draft',
|
||||
Scheduled = 'scheduled',
|
||||
Published = 'published',
|
||||
Offline = 'offline',
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { PublishStatus } from '../drama-status.enum';
|
||||
import { PublishStatus } from './dramas.types';
|
||||
|
||||
@Entity('dramas')
|
||||
export class Drama {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
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 { ConfigureEpisodesDto } from "./dto/configure-episodes.dto";
|
||||
import { CreateDramaDto } from "./dto/create-drama.dto";
|
||||
import { CreateEpisodeDto } from "./dto/create-episode.dto";
|
||||
import { UpdateDramaDto } from "./dto/update-drama.dto";
|
||||
import { UpdateEpisodeDto } from "./dto/update-episode.dto";
|
||||
import { RequirePermissions } from "@app/decorators/require-permissions.decorator";
|
||||
import { AdminJwtAuthGuard } from "@app/guards/admin-jwt-auth.guard";
|
||||
import { PermissionsGuard } from "@app/guards/permissions.guard";
|
||||
import { ConfigureEpisodesDto, CreateDramaDto, CreateEpisodeDto, UpdateDramaDto, UpdateEpisodeDto } from "./dramas.dto";
|
||||
import { DramasService } from "./dramas.service";
|
||||
import { PublishStatus } from "./drama-status.enum";
|
||||
import { PublishStatus } from "./dramas.types";
|
||||
|
||||
@ApiTags("Dramas")
|
||||
@Controller()
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { PublishStatus } from '../drama-status.enum';
|
||||
import { PartialType } from "@nestjs/mapped-types";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsArray, IsBoolean, IsDateString, IsEnum, IsInt, IsOptional, IsString, IsUrl, Max, Min } from "class-validator";
|
||||
import { PublishStatus } from "./dramas.types";
|
||||
|
||||
export class CreateDramaDto {
|
||||
@ApiPropertyOptional()
|
||||
@@ -102,3 +93,148 @@ export class CreateDramaDto {
|
||||
@Min(1)
|
||||
onlineVersion?: number;
|
||||
}
|
||||
|
||||
export class UpdateDramaDto extends PartialType(CreateDramaDto) {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export class UpdateEpisodeDto extends PartialType(CreateEpisodeDto) {}
|
||||
|
||||
export class ConfigureEpisodesDto {
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(500)
|
||||
episodeCount: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { AdminModule } from '@src/admin/admin.module';
|
||||
import { DramasController } from './dramas.controller';
|
||||
import { DramasService } from './dramas.service';
|
||||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { ConflictException, Injectable, NotFoundException, Optional, UnprocessableEntityException } from "@nestjs/common";
|
||||
import { InjectDataSource } from "@nestjs/typeorm";
|
||||
import { DataSource, Like } from "typeorm";
|
||||
import { PaginatedResponse } from "../../common/dto/paginated-response.dto";
|
||||
import { EpisodeMetric } from "../data/entities/episode-metric.entity";
|
||||
import { ConfigureEpisodesDto } from "./dto/configure-episodes.dto";
|
||||
import { CreateDramaDto } from "./dto/create-drama.dto";
|
||||
import { CreateEpisodeDto } from "./dto/create-episode.dto";
|
||||
import { UpdateDramaDto } from "./dto/update-drama.dto";
|
||||
import { UpdateEpisodeDto } from "./dto/update-episode.dto";
|
||||
import { Drama } from "./entities/drama.entity";
|
||||
import { Episode } from "./entities/episode.entity";
|
||||
import { PublishStatus } from "./drama-status.enum";
|
||||
import { DramaRecord, EpisodeRecord } from "./interfaces/drama-records.interface";
|
||||
import { PaginatedResponse } from "@app/common/paginated-response.dto";
|
||||
import { EpisodeMetric } from "@src/data/episode-metric.entity";
|
||||
import { ConfigureEpisodesDto, CreateDramaDto, CreateEpisodeDto, UpdateDramaDto, UpdateEpisodeDto } from "./dramas.dto";
|
||||
import { Drama } from "./drama.entity";
|
||||
import { Episode } from "./episode.entity";
|
||||
import { DramaRecord, EpisodeRecord, PublishStatus } from "./dramas.types";
|
||||
|
||||
interface PaginationInput {
|
||||
page: number;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { PublishStatus } from '../drama-status.enum';
|
||||
export enum PublishStatus {
|
||||
Draft = "draft",
|
||||
Scheduled = "scheduled",
|
||||
Published = "published",
|
||||
Offline = "offline",
|
||||
}
|
||||
|
||||
export interface DramaRecord {
|
||||
id: number;
|
||||
@@ -45,13 +50,13 @@ export interface EpisodeRecord {
|
||||
width?: number;
|
||||
height?: number;
|
||||
isPaid: boolean;
|
||||
freeType: 'free' | 'paid';
|
||||
freeType: "free" | "paid";
|
||||
unlockPrice?: number;
|
||||
price: number;
|
||||
publishAt?: string;
|
||||
nextReleaseAt?: string;
|
||||
status: PublishStatus;
|
||||
reviewStatus: 'not_submitted' | 'pending' | 'reviewing' | 'approved' | 'rejected';
|
||||
reviewStatus: "not_submitted" | "pending" | "reviewing" | "approved" | "rejected";
|
||||
reviewMessage?: string;
|
||||
publishStatus: PublishStatus;
|
||||
createdAt: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { PublishStatus } from '../drama-status.enum';
|
||||
import { PublishStatus } from './dramas.types';
|
||||
|
||||
@Entity('episodes')
|
||||
@Index(['dramaId', 'episodeNo'], { unique: true })
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { UserRecord } from '../users/interfaces/user-record.interface';
|
||||
import { UpsertPlaybackHistoryDto } from './dto/upsert-history.dto';
|
||||
import { CurrentUser } from '@app/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '@app/guards/jwt-auth.guard';
|
||||
import { UserRecord } from '@src/users/users.types';
|
||||
import { UpsertPlaybackHistoryDto } from './history.dto';
|
||||
import { HistoryService } from './history.service';
|
||||
|
||||
@ApiTags('History')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { DramasModule } from '../dramas/dramas.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { AuthModule } from '@src/auth/auth.module';
|
||||
import { DramasModule } from '@src/dramas/dramas.module';
|
||||
import { UsersModule } from '@src/users/users.module';
|
||||
import { HistoryController } from './history.controller';
|
||||
import { HistoryService } from './history.service';
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Injectable, Optional } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
||||
import { DramasService } from '../dramas/dramas.service';
|
||||
import { PaginatedResponse } from '@app/common/paginated-response.dto';
|
||||
import { DramasService } from '@src/dramas/dramas.service';
|
||||
import {
|
||||
DramaRecord,
|
||||
EpisodeRecord,
|
||||
} from '../dramas/interfaces/drama-records.interface';
|
||||
import { UserRecord } from '../users/interfaces/user-record.interface';
|
||||
import { UpsertPlaybackHistoryDto } from './dto/upsert-history.dto';
|
||||
import { UserPlaybackHistory } from './entities/user-playback-history.entity';
|
||||
} from '@src/dramas/dramas.types';
|
||||
import { UserRecord } from '@src/users/users.types';
|
||||
import { UpsertPlaybackHistoryDto } from './history.dto';
|
||||
import { UserPlaybackHistory } from './user-playback-history.entity';
|
||||
import {
|
||||
PlaybackHistoryListItem,
|
||||
PlaybackHistoryRecord,
|
||||
} from './interfaces/history-record.interface';
|
||||
} from './history.types';
|
||||
|
||||
interface PaginationInput {
|
||||
page: number;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 { ApiMessage } from '@app/decorators/api-message.decorator';
|
||||
import { RequirePermissions } from '@app/decorators/require-permissions.decorator';
|
||||
import { AdminJwtAuthGuard } from '@app/guards/admin-jwt-auth.guard';
|
||||
import { PermissionsGuard } from '@app/guards/permissions.guard';
|
||||
import { CreatePlaybackErrorLogDto } from './logs.dto';
|
||||
import { LogsService } from './logs.service';
|
||||
|
||||
@ApiTags('Logs')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { AdminModule } from '@src/admin/admin.module';
|
||||
import { LogsController } from './logs.controller';
|
||||
import { LogsService } from './logs.service';
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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 { PaginatedResponse } from '@app/common/paginated-response.dto';
|
||||
import { CreatePlaybackErrorLogDto } from './logs.dto';
|
||||
import { PlaybackErrorLog } from './playback-error-log.entity';
|
||||
import { RequestLog } from './request-log.entity';
|
||||
import {
|
||||
PlaybackErrorLogRecord,
|
||||
RequestLogRecord,
|
||||
} from './interfaces/log-records.interface';
|
||||
} from './logs.types';
|
||||
|
||||
interface PaginationInput {
|
||||
page: number;
|
||||
|
||||
21
src/main.ts
21
src/main.ts
@@ -1,13 +1,24 @@
|
||||
/**
|
||||
* App main.
|
||||
* @file Index 入口文件
|
||||
* @module src/main
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import "dotenv/config";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module";
|
||||
import { bootstrapApp } from "./bootstrap";
|
||||
import { APP_CONFIG, OPTIONS_CONFIG } from "@config/app.config";
|
||||
import { AppModule } from "@src/app/app.module";
|
||||
import { bootstrapApp } from "@src/bootstrap";
|
||||
|
||||
const logger = new Logger("AppLoader");
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const app = await NestFactory.create(AppModule, OPTIONS_CONFIG);
|
||||
bootstrapApp(app);
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
console.log(`Application is running on: ${await app.getUrl()}`);
|
||||
await app.listen(APP_CONFIG.PORT);
|
||||
logger.log(`Application is running on: ${await app.getUrl()}`);
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
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 { CreateCoverDto } from "./dto/create-cover.dto";
|
||||
import { CreateDirectUploadTaskDto } from "./dto/create-direct-upload-task.dto";
|
||||
import { CreateUploadJobDto } from "./dto/create-upload-job.dto";
|
||||
import { RequirePermissions } from "@app/decorators/require-permissions.decorator";
|
||||
import { AdminJwtAuthGuard } from "@app/guards/admin-jwt-auth.guard";
|
||||
import { PermissionsGuard } from "@app/guards/permissions.guard";
|
||||
import { CreateCoverDto, CreateDirectUploadTaskDto, CreateUploadJobDto } from "./media.dto";
|
||||
import { MediaService } from "./media.service";
|
||||
|
||||
@ApiTags("Media")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsObject, IsOptional, IsString } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsInt, IsObject, IsOptional, IsString, IsUrl, Min } from "class-validator";
|
||||
|
||||
export class CreateUploadJobDto {
|
||||
@ApiProperty()
|
||||
@@ -20,3 +20,40 @@ export class CreateUploadJobDto {
|
||||
@IsObject()
|
||||
rawResponse?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class CreateCoverDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
fileName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsUrl({ require_tld: false })
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class CreateDirectUploadTaskDto {
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
episodeId: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
fileName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
contentType: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
fileSize: number;
|
||||
}
|
||||
|
||||
export class CompleteDirectUploadTaskDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
providerUploadId?: string;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { DramasModule } from '../dramas/dramas.module';
|
||||
import { AdminModule } from '@src/admin/admin.module';
|
||||
import { DramasModule } from '@src/dramas/dramas.module';
|
||||
import { MediaController } from './media.controller';
|
||||
import { MediaService } from './media.service';
|
||||
import { BytePlusMediaProvider } from './providers/byteplus-media.provider';
|
||||
import { BytePlusMediaProvider } from './media.provider';
|
||||
|
||||
@Module({
|
||||
imports: [AdminModule, DramasModule],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BYTEPLUS_CONFIG } from '@config/app.config';
|
||||
|
||||
interface CreateUploadInput {
|
||||
jobId: string;
|
||||
@@ -14,7 +15,7 @@ interface CompleteUploadInput {
|
||||
@Injectable()
|
||||
export class BytePlusMediaProvider {
|
||||
createDirectUpload(input: CreateUploadInput) {
|
||||
const bucket = process.env.BYTEPLUS_BUCKET ?? 'mock-short-drama';
|
||||
const bucket = BYTEPLUS_CONFIG.bucket;
|
||||
const objectKey = `videos/${input.jobId}/${input.fileName}`;
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,18 +2,16 @@ import { Injectable, NotFoundException, Optional } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
||||
import { DramasService } from '../dramas/dramas.service';
|
||||
import { CreateCoverDto } from './dto/create-cover.dto';
|
||||
import { CreateDirectUploadTaskDto } from './dto/create-direct-upload-task.dto';
|
||||
import { CreateUploadJobDto } from './dto/create-upload-job.dto';
|
||||
import { Cover } from './entities/cover.entity';
|
||||
import { MediaUploadJob } from './entities/media-upload-job.entity';
|
||||
import { PaginatedResponse } from '@app/common/paginated-response.dto';
|
||||
import { DramasService } from '@src/dramas/dramas.service';
|
||||
import { CreateCoverDto, CreateDirectUploadTaskDto, CreateUploadJobDto } from './media.dto';
|
||||
import { Cover } from './cover.entity';
|
||||
import { MediaUploadJob } from './media-upload-job.entity';
|
||||
import {
|
||||
CoverRecord,
|
||||
MediaUploadJobRecord,
|
||||
} from './interfaces/media-records.interface';
|
||||
import { BytePlusMediaProvider } from './providers/byteplus-media.provider';
|
||||
} from './media.types';
|
||||
import { BytePlusMediaProvider } from './media.provider';
|
||||
|
||||
interface PaginationInput {
|
||||
page: number;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
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 { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { UserRecord } from '../users/interfaces/user-record.interface';
|
||||
import { CreateOrderDto } from './dto/create-order.dto';
|
||||
import { TikTokPaymentWebhookDto } from './dto/tiktok-payment-webhook.dto';
|
||||
import { RequirePermissions } from '@app/decorators/require-permissions.decorator';
|
||||
import { AdminJwtAuthGuard } from '@app/guards/admin-jwt-auth.guard';
|
||||
import { PermissionsGuard } from '@app/guards/permissions.guard';
|
||||
import { CurrentUser } from '@app/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '@app/guards/jwt-auth.guard';
|
||||
import { UserRecord } from '@src/users/users.types';
|
||||
import { CreateOrderDto, TikTokPaymentWebhookDto } from './orders.dto';
|
||||
import { OrdersService } from './orders.service';
|
||||
|
||||
@ApiTags('Orders')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsInt, Min } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsIn, IsInt, IsOptional, IsString, Min } from "class-validator";
|
||||
|
||||
export class CreateOrderDto {
|
||||
@ApiProperty()
|
||||
@@ -7,3 +7,33 @@ export class CreateOrderDto {
|
||||
@Min(1)
|
||||
episodeId: number;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { DramasModule } from '../dramas/dramas.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { AuthModule } from '@src/auth/auth.module';
|
||||
import { AdminModule } from '@src/admin/admin.module';
|
||||
import { DramasModule } from '@src/dramas/dramas.module';
|
||||
import { UsersModule } from '@src/users/users.module';
|
||||
import { OrdersController } from './orders.controller';
|
||||
import { OrdersService } from './orders.service';
|
||||
|
||||
|
||||
@@ -6,18 +6,17 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
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 { Order } from './entities/order.entity';
|
||||
import { UserEpisodeUnlock } from './entities/user-episode-unlock.entity';
|
||||
import { PaginatedResponse } from '@app/common/paginated-response.dto';
|
||||
import { DramasService } from '@src/dramas/dramas.service';
|
||||
import { UserRecord } from '@src/users/users.types';
|
||||
import { CreateOrderDto, TikTokPaymentWebhookDto } from './orders.dto';
|
||||
import { Order } from './order.entity';
|
||||
import { UserEpisodeUnlock } from './user-episode-unlock.entity';
|
||||
import {
|
||||
OrderRecord,
|
||||
OrderStatus,
|
||||
UserEpisodeUnlockRecord,
|
||||
} from './interfaces/order-records.interface';
|
||||
} from './orders.types';
|
||||
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { UserRecord } from '../users/interfaces/user-record.interface';
|
||||
import { CurrentUser } from '@app/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '@app/guards/jwt-auth.guard';
|
||||
import { UserRecord } from '@src/users/users.types';
|
||||
import { PlayerService } from './player.service';
|
||||
|
||||
@ApiTags('Player')
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { DramasModule } from '../dramas/dramas.module';
|
||||
import { OrdersModule } from '../orders/orders.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { AuthModule } from '@src/auth/auth.module';
|
||||
import { DramasModule } from '@src/dramas/dramas.module';
|
||||
import { OrdersModule } from '@src/orders/orders.module';
|
||||
import { UsersModule } from '@src/users/users.module';
|
||||
import { EpisodePlayerController, PlayerController } from './player.controller';
|
||||
import { PlayerService } from './player.service';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DramasService } from '../dramas/dramas.service';
|
||||
import { OrdersService } from '../orders/orders.service';
|
||||
import { UserRecord } from '../users/interfaces/user-record.interface';
|
||||
import { DramasService } from '@src/dramas/dramas.service';
|
||||
import { OrdersService } from '@src/orders/orders.service';
|
||||
import { UserRecord } from '@src/users/users.types';
|
||||
|
||||
@Injectable()
|
||||
export class PlayerService {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Body, Controller, Get, Param, Patch, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
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 { UpdateUserStatusDto } from './dto/update-user-status.dto';
|
||||
import { RequirePermissions } from '@app/decorators/require-permissions.decorator';
|
||||
import { AdminJwtAuthGuard } from '@app/guards/admin-jwt-auth.guard';
|
||||
import { PermissionsGuard } from '@app/guards/permissions.guard';
|
||||
import { UpdateUserStatusDto } from './users.dto';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@ApiTags('Users')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { AdminModule } from '@src/admin/admin.module';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { User } from "./entities/user.entity";
|
||||
import { UserRecord } from "./interfaces/user-record.interface";
|
||||
import { PaginatedResponse } from "@app/common/paginated-response.dto";
|
||||
import { User } from "./user.entity";
|
||||
import { UserRecord } from "./users.types";
|
||||
|
||||
interface UpsertTikTokUserInput {
|
||||
tiktokOpenId: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { AppModule } from '../src/app/app.module';
|
||||
import { bootstrapApp } from '../src/bootstrap';
|
||||
import { loginAdmin } from './test-helpers';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { validate as validateUuid, version as uuidVersion } from 'uuid';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { AppModule } from '../src/app/app.module';
|
||||
import { bootstrapApp } from '../src/bootstrap';
|
||||
import { loginAdmin } from './test-helpers';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { AppModule } from '../src/app/app.module';
|
||||
import { bootstrapApp } from '../src/bootstrap';
|
||||
import { loginAdmin } from './test-helpers';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { AppModule } from '../src/app/app.module';
|
||||
import { bootstrapApp } from '../src/bootstrap';
|
||||
import { loginAdmin } from './test-helpers';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AdminService } from '../src/modules/admin/admin.service';
|
||||
import { DataService } from '../src/modules/data/data.service';
|
||||
import { TikTokDataProvider } from '../src/modules/data/providers/tiktok-data.provider';
|
||||
import { PublishStatus } from '../src/modules/dramas/drama-status.enum';
|
||||
import { DramasService } from '../src/modules/dramas/dramas.service';
|
||||
import { LogsService } from '../src/modules/logs/logs.service';
|
||||
import { UsersService } from '../src/modules/users/users.service';
|
||||
import { AdminService } from '../src/admin/admin.service';
|
||||
import { DataService } from '../src/data/data.service';
|
||||
import { TikTokDataProvider } from '../src/data/data.provider';
|
||||
import { PublishStatus } from '../src/dramas/dramas.types';
|
||||
import { DramasService } from '../src/dramas/dramas.service';
|
||||
import { LogsService } from '../src/logs/logs.service';
|
||||
import { UsersService } from '../src/users/users.service';
|
||||
|
||||
function createRepository<T extends { id?: number }>() {
|
||||
const records: T[] = [];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { AppModule } from '../src/app/app.module';
|
||||
import { bootstrapApp } from '../src/bootstrap';
|
||||
import { loginAdmin } from './test-helpers';
|
||||
|
||||
|
||||
@@ -6,5 +6,11 @@
|
||||
"watchman": false,
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"^@src/(.*)$": "<rootDir>/src/$1",
|
||||
"^@config/(.*)$": "<rootDir>/config/$1",
|
||||
"^@constants/(.*)$": "<rootDir>/constants/$1",
|
||||
"^@app/(.*)$": "<rootDir>/$1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { AppModule } from '../src/app/app.module';
|
||||
import { bootstrapApp } from '../src/bootstrap';
|
||||
import { loginAdmin } from './test-helpers';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { AppModule } from '../src/app/app.module';
|
||||
import { bootstrapApp } from '../src/bootstrap';
|
||||
import { loginAdmin } from './test-helpers';
|
||||
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"noImplicitAny": false
|
||||
"noImplicitAny": false,
|
||||
"paths": {
|
||||
"@app/*": ["./*"],
|
||||
"@src/*": ["./src/*"],
|
||||
"@config/*": ["./config/*"],
|
||||
"@constants/*": ["./constants/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Request } from 'express';
|
||||
/**
|
||||
* Request context util.
|
||||
* @file 请求上下文工具
|
||||
* @module utils/request-context.util
|
||||
* @author zhxiao1124
|
||||
*/
|
||||
|
||||
import { Request } from "express";
|
||||
|
||||
export function getRequestCost(request: Request & { startTime?: bigint }) {
|
||||
if (!request.startTime) {
|
||||
|
||||
Reference in New Issue
Block a user