diff --git a/common/paginated-response.dto.ts b/common/paginated-response.dto.ts index d0fb597..145df6a 100644 --- a/common/paginated-response.dto.ts +++ b/common/paginated-response.dto.ts @@ -1,10 +1,17 @@ +/** + * Paginated response dto. + * @file 通用分页响应结构 + * @module common/paginated-response.dto + * @author zhxiao1124 + */ + export interface PaginationMeta { - page: number; - pageSize: number; - total: number; + page: number; + pageSize: number; + total: number; } export interface PaginatedResponse { - list: T[]; - pagination: PaginationMeta; + list: T[]; + pagination: PaginationMeta; } diff --git a/config/app.config.ts b/config/app.config.ts new file mode 100644 index 0000000..9ab29b8 --- /dev/null +++ b/config/app.config.ts @@ -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", +}; diff --git a/constants/text.constant.ts b/constants/text.constant.ts index f62a111..3090750 100644 --- a/constants/text.constant.ts +++ b/constants/text.constant.ts @@ -1 +1,8 @@ -export const SUCCESS_MESSAGE = 'success'; +/** + * Text constant. + * @file 通用文案常量 + * @module constants/text.constant + * @author zhxiao1124 + */ + +export const SUCCESS_MESSAGE = "success"; diff --git a/decorators/api-message.decorator.ts b/decorators/api-message.decorator.ts index f34674f..de67457 100644 --- a/decorators/api-message.decorator.ts +++ b/decorators/api-message.decorator.ts @@ -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); diff --git a/decorators/current-admin.decorator.ts b/decorators/current-admin.decorator.ts index b2023ba..3794a34 100644 --- a/decorators/current-admin.decorator.ts +++ b/decorators/current-admin.decorator.ts @@ -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(); +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(); return request.adminUser; - }, -); +}); diff --git a/decorators/current-user.decorator.ts b/decorators/current-user.decorator.ts index 776fdf6..e7605f9 100644 --- a/decorators/current-user.decorator.ts +++ b/decorators/current-user.decorator.ts @@ -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(); return request.user; - }, -); +}); diff --git a/decorators/require-permissions.decorator.ts b/decorators/require-permissions.decorator.ts index ce673ad..fe947ad 100644 --- a/decorators/require-permissions.decorator.ts +++ b/decorators/require-permissions.decorator.ts @@ -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); diff --git a/eslint.config.mjs b/eslint.config.mjs index 2bf4f90..3cbe7eb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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', diff --git a/filters/http.filter.ts b/filters/http.filter.ts index f1efe8d..059924f 100644 --- a/filters/http.filter.ts +++ b/filters/http.filter.ts @@ -1,79 +1,71 @@ -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 { - constructor(private readonly logsService: LogsService) {} + constructor(private readonly logsService: LogsService) {} - catch(exception: unknown, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const request = ctx.getRequest< - Request & { requestId?: string; adminId?: number; user?: { id: number } } - >(); - const response = ctx.getResponse(); - const statusCode = - exception instanceof HttpException - ? exception.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; - const message = this.getMessage(exception, statusCode); - const requestId = request.requestId ?? ''; - const costMs = getRequestCost(request); + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + const statusCode = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + const message = this.getMessage(exception, statusCode); + const requestId = request.requestId ?? ""; + const costMs = getRequestCost(request); - this.logsService.recordRequest({ - requestId, - method: request.method, - path: request.originalUrl ?? request.url, - query: request.query, - body: request.body, - statusCode, - responseCode: statusCode, - message, - costMs, - userId: request.user?.id, - adminId: request.adminId, - ip: request.ip, - userAgent: request.get('user-agent'), - errorStack: exception instanceof Error ? exception.stack : undefined, - }); + this.logsService.recordRequest({ + requestId, + method: request.method, + path: request.originalUrl ?? request.url, + query: request.query, + body: request.body, + statusCode, + responseCode: statusCode, + message, + costMs, + userId: request.user?.id, + adminId: request.adminId, + ip: request.ip, + userAgent: request.get("user-agent"), + errorStack: exception instanceof Error ? exception.stack : undefined, + }); - response.status(statusCode).json({ - code: statusCode, - data: {}, - message, - timestamp: Date.now(), - requestId, - cost: `${costMs}ms`, - }); - } - - private getMessage(exception: unknown, statusCode: number): string { - if (exception instanceof HttpException) { - const response = exception.getResponse(); - if (typeof response === 'string') { - return response; - } - if ( - response && - typeof response === 'object' && - 'message' in response - ) { - const message = (response as { message: string | string[] }).message; - return Array.isArray(message) ? message.join('; ') : message; - } + response.status(statusCode).json({ + code: statusCode, + data: {}, + message, + timestamp: Date.now(), + requestId, + cost: `${costMs}ms`, + }); } - if (exception instanceof Error && statusCode === HttpStatus.INTERNAL_SERVER_ERROR) { - return exception.message || 'Internal server error'; - } + private getMessage(exception: unknown, statusCode: number): string { + if (exception instanceof HttpException) { + const response = exception.getResponse(); + if (typeof response === "string") { + return 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 'Internal server error'; - } + if (exception instanceof Error && statusCode === HttpStatus.INTERNAL_SERVER_ERROR) { + return exception.message || "Internal server error"; + } + + return "Internal server error"; + } } diff --git a/guards/admin-jwt-auth.guard.ts b/guards/admin-jwt-auth.guard.ts index 6f9cbd4..17601a4 100644 --- a/guards/admin-jwt-auth.guard.ts +++ b/guards/admin-jwt-auth.guard.ts @@ -1,53 +1,51 @@ -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 { - constructor( - private readonly jwtService: JwtService, - private readonly adminService: AdminService, - ) {} + constructor( + private readonly jwtService: JwtService, + private readonly adminService: AdminService, + ) {} - async canActivate(context: ExecutionContext): Promise { - const request = context - .switchToHttp() - .getRequest(); - const authHeader = request.get('authorization'); - const token = authHeader?.startsWith('Bearer ') - ? authHeader.slice('Bearer '.length) - : undefined; + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.get("authorization"); + const token = authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : undefined; - if (!token) { - throw new UnauthorizedException('Admin not authenticated'); + if (!token) { + throw new UnauthorizedException("Admin not authenticated"); + } + + try { + const payload = await this.jwtService.verifyAsync<{ + sub: number; + type: string; + }>(token); + 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"); + } + + request.adminUser = adminUser; + request.adminId = adminUser.id; + return true; + } catch { + throw new UnauthorizedException("Admin not authenticated"); + } } - - try { - const payload = await this.jwtService.verifyAsync<{ - sub: number; - type: string; - }>(token); - 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'); - } - - request.adminUser = adminUser; - request.adminId = adminUser.id; - return true; - } catch { - throw new UnauthorizedException('Admin not authenticated'); - } - } } diff --git a/guards/jwt-auth.guard.ts b/guards/jwt-auth.guard.ts index 3fb8931..333a2a8 100644 --- a/guards/jwt-auth.guard.ts +++ b/guards/jwt-auth.guard.ts @@ -1,42 +1,42 @@ -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 { - constructor( - private readonly jwtService: JwtService, - private readonly usersService: UsersService, - ) {} + constructor( + private readonly jwtService: JwtService, + private readonly usersService: UsersService, + ) {} - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const authHeader = request.get('authorization'); - const token = authHeader?.startsWith('Bearer ') - ? authHeader.slice('Bearer '.length) - : undefined; + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.get("authorization"); + const token = authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : undefined; - if (!token) { - throw new UnauthorizedException('Not authenticated'); + if (!token) { + 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"); + } + + request.user = user; + return true; + } catch { + 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'); - } - - request.user = user; - return true; - } catch { - throw new UnauthorizedException('Not authenticated'); - } - } } diff --git a/guards/permissions.guard.ts b/guards/permissions.guard.ts index 81fab69..2e255c5 100644 --- a/guards/permissions.guard.ts +++ b/guards/permissions.guard.ts @@ -1,54 +1,49 @@ -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 { - constructor( - private readonly reflector: Reflector, - private readonly adminService: AdminService, - ) {} + constructor( + private readonly reflector: Reflector, + private readonly adminService: AdminService, + ) {} - async canActivate(context: ExecutionContext): Promise { - const required = - this.reflector.getAllAndOverride( - REQUIRED_PERMISSIONS_METADATA, - [context.getHandler(), context.getClass()], - ) ?? []; + async canActivate(context: ExecutionContext): Promise { + const required = this.reflector.getAllAndOverride(REQUIRED_PERMISSIONS_METADATA, [context.getHandler(), context.getClass()]) ?? []; - if (required.length === 0) { - return true; + if (required.length === 0) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const adminUser = request.adminUser; + + if (!adminUser) { + 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)); + + if (!allowed) { + throw new ForbiddenException("No permission"); + } + + return true; } - - const request = context - .switchToHttp() - .getRequest(); - const adminUser = request.adminUser; - - if (!adminUser) { - 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), - ); - - if (!allowed) { - throw new ForbiddenException('No permission'); - } - - return true; - } } diff --git a/interceptor/logger.interceptor.ts b/interceptor/logger.interceptor.ts index c301337..d990f35 100644 --- a/interceptor/logger.interceptor.ts +++ b/interceptor/logger.interceptor.ts @@ -1,55 +1,51 @@ -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 { - constructor( - private readonly logsService: LogsService, - private readonly reflector: Reflector, - ) {} +export class LoggerInterceptor implements NestInterceptor { + constructor( + private readonly logsService: LogsService, + private readonly reflector: Reflector, + ) {} - intercept(context: ExecutionContext, next: CallHandler): Observable { - const http = context.switchToHttp(); - const request = http.getRequest< - Request & { requestId?: string; adminId?: number; user?: { id: number } } - >(); - const response = http.getResponse(); - const message = - this.reflector.get(API_MESSAGE_METADATA, context.getHandler()) ?? - SUCCESS_MESSAGE; + intercept(context: ExecutionContext, next: CallHandler): Observable { + const http = context.switchToHttp(); + const request = http.getRequest(); + const response = http.getResponse(); + const message = this.reflector.get(API_MESSAGE_METADATA, context.getHandler()) ?? SUCCESS_MESSAGE; - return next.handle().pipe( - tap(() => { - this.logsService.recordRequest({ - requestId: request.requestId ?? '', - method: request.method, - path: request.originalUrl ?? request.url, - query: request.query, - body: request.body, - statusCode: response.statusCode, - responseCode: 0, - message, - costMs: getRequestCost(request), - userId: request.user?.id, - adminId: request.adminId, - ip: request.ip, - userAgent: request.get('user-agent'), - errorStack: undefined, - }); - }), - ); - } + return next.handle().pipe( + tap(() => { + this.logsService.recordRequest({ + requestId: request.requestId ?? "", + method: request.method, + path: request.originalUrl ?? request.url, + query: request.query, + body: request.body, + statusCode: response.statusCode, + responseCode: 0, + message, + costMs: getRequestCost(request), + userId: request.user?.id, + adminId: request.adminId, + ip: request.ip, + userAgent: request.get("user-agent"), + errorStack: undefined, + }); + }), + ); + } } diff --git a/interceptor/response.interceptor.ts b/interceptor/response.interceptor.ts index 8b5ec1b..2b590a4 100644 --- a/interceptor/response.interceptor.ts +++ b/interceptor/response.interceptor.ts @@ -1,39 +1,35 @@ -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()) {} + constructor(private readonly reflector = new Reflector()) {} - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context - .switchToHttp() - .getRequest(); - const message = - this.reflector.get(API_MESSAGE_METADATA, context.getHandler()) ?? - SUCCESS_MESSAGE; + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const message = this.reflector.get(API_MESSAGE_METADATA, context.getHandler()) ?? SUCCESS_MESSAGE; - return next.handle().pipe( - map((data) => ({ - code: 0, - data: data ?? {}, - message, - timestamp: Date.now(), - requestId: request.requestId, - cost: `${getRequestCost(request)}ms`, - })), - ); - } + return next.handle().pipe( + map((data) => ({ + code: 0, + data: data ?? {}, + message, + timestamp: Date.now(), + requestId: request.requestId, + cost: `${getRequestCost(request)}ms`, + })), + ); + } } diff --git a/middleware/request-id.middleware.ts b/middleware/request-id.middleware.ts index d3aea6b..c1e2147 100644 --- a/middleware/request-id.middleware.ts +++ b/middleware/request-id.middleware.ts @@ -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"; - request.requestId = requestId; - request.startTime = process.hrtime.bigint(); - response.setHeader('X-Request-Id', requestId); - next(); +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); + next(); } diff --git a/package.json b/package.json index f5e60f6..1ac0ce9 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 69ade1a..e4d55c2 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -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') diff --git a/src/admin/admin.dto.ts b/src/admin/admin.dto.ts index 878d235..2e57266 100644 --- a/src/admin/admin.dto.ts +++ b/src/admin/admin.dto.ts @@ -1,13 +1,123 @@ -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() - @IsString() - username: string; + @ApiProperty() + @IsString() + username: string; - @ApiProperty() - @IsString() - @MinLength(6) - password: string; + @ApiProperty() + @IsString() + @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[]; } diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index 472ab37..1ca01df 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -1,20 +1,21 @@ -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' }, - }), - ], - controllers: [AdminController], - providers: [AdminService, AdminJwtAuthGuard, PermissionsGuard], - exports: [JwtModule, AdminService, AdminJwtAuthGuard, PermissionsGuard], + imports: [ + JwtModule.register({ + secret: ADMIN_AUTH_CONFIG.jwtTokenSecret, + signOptions: { expiresIn: ADMIN_AUTH_CONFIG.expiresIn }, + }), + ], + controllers: [AdminController], + providers: [AdminService, AdminJwtAuthGuard, PermissionsGuard], + exports: [JwtModule, AdminService, AdminJwtAuthGuard, PermissionsGuard], }) export class AdminModule {} diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 72e677d..aa0e0d6 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -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: [], diff --git a/src/app/app.controller.ts b/src/app/app.controller.ts index 6f8d2f3..fa7751b 100644 --- a/src/app/app.controller.ts +++ b/src/app/app.controller.ts @@ -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 { - @Get() - getHealth() { - return { status: 'ok' }; - } +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" }; + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9cd363a..220c64a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 210fd95..9c04b66 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -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') diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 9f6a7f4..969ed84 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,20 +1,21 @@ -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' }, - }), - ], - controllers: [AuthController], - providers: [AuthService, JwtAuthGuard], - exports: [JwtModule, JwtAuthGuard], + imports: [ + UsersModule, + JwtModule.register({ + secret: AUTH_CONFIG.jwtTokenSecret, + signOptions: { expiresIn: AUTH_CONFIG.expiresIn }, + }), + ], + controllers: [AuthController], + providers: [AuthService, JwtAuthGuard], + exports: [JwtModule, JwtAuthGuard], }) export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index cd7ac37..ad9c7e5 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -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 { diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 2429517..0a15cae 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -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,11 +27,11 @@ 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(); - + const document = SwaggerModule.createDocument(app, config); - + SwaggerModule.setup("/api/docs", app, document); } diff --git a/src/data/data.controller.ts b/src/data/data.controller.ts index 28b9396..cf4aad1 100644 --- a/src/data/data.controller.ts +++ b/src/data/data.controller.ts @@ -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') diff --git a/src/data/data.module.ts b/src/data/data.module.ts index ebf7285..88b3f22 100644 --- a/src/data/data.module.ts +++ b/src/data/data.module.ts @@ -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], diff --git a/src/data/data.provider.ts b/src/data/data.provider.ts index f640d98..3347846 100644 --- a/src/data/data.provider.ts +++ b/src/data/data.provider.ts @@ -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 { diff --git a/src/data/data.service.ts b/src/data/data.service.ts index a64e565..2d0865a 100644 --- a/src/data/data.service.ts +++ b/src/data/data.service.ts @@ -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; diff --git a/src/dramas/drama-status.enum.ts b/src/dramas/drama-status.enum.ts deleted file mode 100644 index e3a723e..0000000 --- a/src/dramas/drama-status.enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum PublishStatus { - Draft = 'draft', - Scheduled = 'scheduled', - Published = 'published', - Offline = 'offline', -} diff --git a/src/dramas/drama.entity.ts b/src/dramas/drama.entity.ts index 6abfc30..72aa9e5 100644 --- a/src/dramas/drama.entity.ts +++ b/src/dramas/drama.entity.ts @@ -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 { diff --git a/src/dramas/dramas.controller.ts b/src/dramas/dramas.controller.ts index 7ccdde2..29bb9f7 100644 --- a/src/dramas/dramas.controller.ts +++ b/src/dramas/dramas.controller.ts @@ -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() diff --git a/src/dramas/dramas.dto.ts b/src/dramas/dramas.dto.ts index 4c01cf6..61d3b00 100644 --- a/src/dramas/dramas.dto.ts +++ b/src/dramas/dramas.dto.ts @@ -1,104 +1,240 @@ -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() - @IsOptional() - @IsString() - tiktokAlbumId?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + tiktokAlbumId?: string; - @ApiProperty() - @IsString() - title: string; + @ApiProperty() + @IsString() + title: string; - @ApiPropertyOptional() - @IsOptional() - @IsString() - description?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + description?: string; - @ApiProperty() - @IsOptional() - @IsUrl({ require_tld: false }) - coverUrl: string; + @ApiProperty() + @IsOptional() + @IsUrl({ require_tld: false }) + coverUrl: string; - @ApiPropertyOptional() - @IsOptional() - @IsInt() - @Min(1) - coverId?: number; + @ApiPropertyOptional() + @IsOptional() + @IsInt() + @Min(1) + coverId?: number; - @ApiPropertyOptional() - @IsOptional() - @IsUrl({ require_tld: false }) - portraitCoverUrl?: string; + @ApiPropertyOptional() + @IsOptional() + @IsUrl({ require_tld: false }) + portraitCoverUrl?: string; - @ApiPropertyOptional() - @IsOptional() - @IsUrl({ require_tld: false }) - landscapeCoverUrl?: string; + @ApiPropertyOptional() + @IsOptional() + @IsUrl({ require_tld: false }) + landscapeCoverUrl?: string; - @ApiProperty() - @IsString() - type: string; + @ApiProperty() + @IsString() + type: string; - @ApiPropertyOptional({ type: [String] }) - @IsOptional() - @IsArray() - @IsString({ each: true }) - tags?: string[]; + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; - @ApiPropertyOptional() - @IsOptional() - @IsString() - region?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + region?: string; - @ApiPropertyOptional() - @IsOptional() - @IsString() - language?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + language?: string; - @ApiPropertyOptional() - @IsOptional() - @IsInt() - @Min(1900) - year?: number; + @ApiPropertyOptional() + @IsOptional() + @IsInt() + @Min(1900) + year?: number; - @ApiPropertyOptional({ enum: PublishStatus }) - @IsOptional() - @IsEnum(PublishStatus) - status?: PublishStatus; + @ApiPropertyOptional({ enum: PublishStatus }) + @IsOptional() + @IsEnum(PublishStatus) + status?: PublishStatus; - @ApiPropertyOptional() - @IsOptional() - @IsInt() - sortOrder?: number; + @ApiPropertyOptional() + @IsOptional() + @IsInt() + sortOrder?: number; - @ApiPropertyOptional() - @IsOptional() - @IsBoolean() - isRecommended?: boolean; + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + isRecommended?: boolean; - @ApiPropertyOptional() - @IsOptional() - @IsInt() - @Min(1) - @Max(500) - totalEpisodes?: number; + @ApiPropertyOptional() + @IsOptional() + @IsInt() + @Min(1) + @Max(500) + totalEpisodes?: number; - @ApiPropertyOptional() - @IsOptional() - @IsInt() - @Min(1) - onlineVersion?: number; + @ApiPropertyOptional() + @IsOptional() + @IsInt() + @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; } diff --git a/src/dramas/dramas.module.ts b/src/dramas/dramas.module.ts index 07a8cd1..f6d2a38 100644 --- a/src/dramas/dramas.module.ts +++ b/src/dramas/dramas.module.ts @@ -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'; diff --git a/src/dramas/dramas.service.ts b/src/dramas/dramas.service.ts index 0b58582..61f8958 100644 --- a/src/dramas/dramas.service.ts +++ b/src/dramas/dramas.service.ts @@ -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; diff --git a/src/dramas/dramas.types.ts b/src/dramas/dramas.types.ts index f7817ea..f3b76c7 100644 --- a/src/dramas/dramas.types.ts +++ b/src/dramas/dramas.types.ts @@ -1,59 +1,64 @@ -import { PublishStatus } from '../drama-status.enum'; +export enum PublishStatus { + Draft = "draft", + Scheduled = "scheduled", + Published = "published", + Offline = "offline", +} export interface DramaRecord { - id: number; - tiktokAlbumId?: string; - title: string; - description?: string; - coverId?: number; - coverUrl: string; - portraitCoverUrl?: string; - landscapeCoverUrl?: string; - type: string; - tags: string[]; - region?: string; - language?: string; - year?: number; - status: PublishStatus; - sortOrder: number; - isRecommended: boolean; - onlineVersion: number; - totalEpisodes: number; - publishedEpisodes: number; - plays: number; - createdAt: string; - updatedAt: string; + id: number; + tiktokAlbumId?: string; + title: string; + description?: string; + coverId?: number; + coverUrl: string; + portraitCoverUrl?: string; + landscapeCoverUrl?: string; + type: string; + tags: string[]; + region?: string; + language?: string; + year?: number; + status: PublishStatus; + sortOrder: number; + isRecommended: boolean; + onlineVersion: number; + totalEpisodes: number; + publishedEpisodes: number; + plays: number; + createdAt: string; + updatedAt: string; } export interface EpisodeRecord { - id: number; - dramaId: number; - albumId: number; - tiktokEpisodeId?: string; - episodeNo: number; - seq: number; - title: string; - description?: string; - byteplusVid?: string; - coverId?: number; - coverUrl: string; - videoUrl?: string; - storageBucket?: string; - objectKey?: string; - trialDurationSeconds?: number; - durationSeconds: number; - width?: number; - height?: number; - isPaid: boolean; - freeType: 'free' | 'paid'; - unlockPrice?: number; - price: number; - publishAt?: string; - nextReleaseAt?: string; - status: PublishStatus; - reviewStatus: 'not_submitted' | 'pending' | 'reviewing' | 'approved' | 'rejected'; - reviewMessage?: string; - publishStatus: PublishStatus; - createdAt: string; - updatedAt: string; + id: number; + dramaId: number; + albumId: number; + tiktokEpisodeId?: string; + episodeNo: number; + seq: number; + title: string; + description?: string; + byteplusVid?: string; + coverId?: number; + coverUrl: string; + videoUrl?: string; + storageBucket?: string; + objectKey?: string; + trialDurationSeconds?: number; + durationSeconds: number; + width?: number; + height?: number; + isPaid: boolean; + freeType: "free" | "paid"; + unlockPrice?: number; + price: number; + publishAt?: string; + nextReleaseAt?: string; + status: PublishStatus; + reviewStatus: "not_submitted" | "pending" | "reviewing" | "approved" | "rejected"; + reviewMessage?: string; + publishStatus: PublishStatus; + createdAt: string; + updatedAt: string; } diff --git a/src/dramas/episode.entity.ts b/src/dramas/episode.entity.ts index 5191578..46bb030 100644 --- a/src/dramas/episode.entity.ts +++ b/src/dramas/episode.entity.ts @@ -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 }) diff --git a/src/history/history.controller.ts b/src/history/history.controller.ts index ff51028..81f9d84 100644 --- a/src/history/history.controller.ts +++ b/src/history/history.controller.ts @@ -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') diff --git a/src/history/history.module.ts b/src/history/history.module.ts index 25df03c..8d8ec9a 100644 --- a/src/history/history.module.ts +++ b/src/history/history.module.ts @@ -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'; diff --git a/src/history/history.service.ts b/src/history/history.service.ts index 7554908..01fadb0 100644 --- a/src/history/history.service.ts +++ b/src/history/history.service.ts @@ -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; diff --git a/src/logs/logs.controller.ts b/src/logs/logs.controller.ts index f91a3ad..48ccfbc 100644 --- a/src/logs/logs.controller.ts +++ b/src/logs/logs.controller.ts @@ -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') diff --git a/src/logs/logs.module.ts b/src/logs/logs.module.ts index 27651b8..5e753cd 100644 --- a/src/logs/logs.module.ts +++ b/src/logs/logs.module.ts @@ -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'; diff --git a/src/logs/logs.service.ts b/src/logs/logs.service.ts index 601e138..4c715a8 100644 --- a/src/logs/logs.service.ts +++ b/src/logs/logs.service.ts @@ -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; diff --git a/src/main.ts b/src/main.ts index d99b050..8411021 100644 --- a/src/main.ts +++ b/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(); diff --git a/src/media/media.controller.ts b/src/media/media.controller.ts index 586de67..d77aabc 100644 --- a/src/media/media.controller.ts +++ b/src/media/media.controller.ts @@ -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") diff --git a/src/media/media.dto.ts b/src/media/media.dto.ts index 9d8fef9..5b826b5 100644 --- a/src/media/media.dto.ts +++ b/src/media/media.dto.ts @@ -1,22 +1,59 @@ -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() - @IsString() - jobId: string; + @ApiProperty() + @IsString() + jobId: string; - @ApiPropertyOptional() - @IsOptional() - @IsString() - byteplusVid?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + byteplusVid?: string; - @ApiProperty() - @IsString() - status: string; + @ApiProperty() + @IsString() + status: string; - @ApiPropertyOptional() - @IsOptional() - @IsObject() - rawResponse?: Record; + @ApiPropertyOptional() + @IsOptional() + @IsObject() + rawResponse?: Record; +} + +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; } diff --git a/src/media/media.module.ts b/src/media/media.module.ts index 49afd7f..ad98ef4 100644 --- a/src/media/media.module.ts +++ b/src/media/media.module.ts @@ -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], diff --git a/src/media/media.provider.ts b/src/media/media.provider.ts index 5fdff53..0d3e04f 100644 --- a/src/media/media.provider.ts +++ b/src/media/media.provider.ts @@ -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 { diff --git a/src/media/media.service.ts b/src/media/media.service.ts index ba906a5..ca9d943 100644 --- a/src/media/media.service.ts +++ b/src/media/media.service.ts @@ -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; diff --git a/src/orders/orders.controller.ts b/src/orders/orders.controller.ts index 36f70bd..8721381 100644 --- a/src/orders/orders.controller.ts +++ b/src/orders/orders.controller.ts @@ -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') diff --git a/src/orders/orders.dto.ts b/src/orders/orders.dto.ts index ea134cd..8e6a165 100644 --- a/src/orders/orders.dto.ts +++ b/src/orders/orders.dto.ts @@ -1,9 +1,39 @@ -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() - @IsInt() - @Min(1) - episodeId: number; + @ApiProperty() + @IsInt() + @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; } diff --git a/src/orders/orders.module.ts b/src/orders/orders.module.ts index 563eb4e..bcde614 100644 --- a/src/orders/orders.module.ts +++ b/src/orders/orders.module.ts @@ -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'; diff --git a/src/orders/orders.service.ts b/src/orders/orders.service.ts index 84565e6..448a2a7 100644 --- a/src/orders/orders.service.ts +++ b/src/orders/orders.service.ts @@ -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 { diff --git a/src/player/player.controller.ts b/src/player/player.controller.ts index bbf442b..950cf05 100644 --- a/src/player/player.controller.ts +++ b/src/player/player.controller.ts @@ -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') diff --git a/src/player/player.module.ts b/src/player/player.module.ts index 135a17b..04ceed5 100644 --- a/src/player/player.module.ts +++ b/src/player/player.module.ts @@ -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'; diff --git a/src/player/player.service.ts b/src/player/player.service.ts index dc84359..516dc87 100644 --- a/src/player/player.service.ts +++ b/src/player/player.service.ts @@ -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 { diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index c27bf84..acdad5a 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -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') diff --git a/src/users/users.module.ts b/src/users/users.module.ts index fbb7856..8e2d145 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -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'; diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 53edc0b..fe1455c 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -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; diff --git a/test/admin-permissions-users.e2e-spec.ts b/test/admin-permissions-users.e2e-spec.ts index 3c5b7c5..fae62d0 100644 --- a/test/admin-permissions-users.e2e-spec.ts +++ b/test/admin-permissions-users.e2e-spec.ts @@ -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'; diff --git a/test/api-standard.e2e-spec.ts b/test/api-standard.e2e-spec.ts index 739a947..196acfe 100644 --- a/test/api-standard.e2e-spec.ts +++ b/test/api-standard.e2e-spec.ts @@ -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'; diff --git a/test/core-modules.e2e-spec.ts b/test/core-modules.e2e-spec.ts index 797f744..1c68593 100644 --- a/test/core-modules.e2e-spec.ts +++ b/test/core-modules.e2e-spec.ts @@ -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'; diff --git a/test/data-media-flow.e2e-spec.ts b/test/data-media-flow.e2e-spec.ts index 54f15be..416f9bf 100644 --- a/test/data-media-flow.e2e-spec.ts +++ b/test/data-media-flow.e2e-spec.ts @@ -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'; diff --git a/test/database-persistence.e2e-spec.ts b/test/database-persistence.e2e-spec.ts index 2f1bd62..0c4ce63 100644 --- a/test/database-persistence.e2e-spec.ts +++ b/test/database-persistence.e2e-spec.ts @@ -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() { const records: T[] = []; diff --git a/test/dramas.e2e-spec.ts b/test/dramas.e2e-spec.ts index 496613b..5bf2028 100644 --- a/test/dramas.e2e-spec.ts +++ b/test/dramas.e2e-spec.ts @@ -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'; diff --git a/test/jest-e2e.json b/test/jest-e2e.json index 8032392..d292743 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -6,5 +6,11 @@ "watchman": false, "transform": { "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "^@src/(.*)$": "/src/$1", + "^@config/(.*)$": "/config/$1", + "^@constants/(.*)$": "/constants/$1", + "^@app/(.*)$": "/$1" } } diff --git a/test/orders-playback.e2e-spec.ts b/test/orders-playback.e2e-spec.ts index 8a91876..6b1e728 100644 --- a/test/orders-playback.e2e-spec.ts +++ b/test/orders-playback.e2e-spec.ts @@ -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'; diff --git a/test/playback-error.e2e-spec.ts b/test/playback-error.e2e-spec.ts index 959acbe..d325c18 100644 --- a/test/playback-error.e2e-spec.ts +++ b/test/playback-error.e2e-spec.ts @@ -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'; diff --git a/tsconfig.json b/tsconfig.json index 8acc204..a66eb30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] } diff --git a/utils/request-context.util.ts b/utils/request-context.util.ts index 05f97b8..0ecb6d8 100644 --- a/utils/request-context.util.ts +++ b/utils/request-context.util.ts @@ -1,10 +1,17 @@ -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) { - return 0; - } + if (!request.startTime) { + return 0; + } - const diff = process.hrtime.bigint() - request.startTime; - return Number(diff / BigInt(1_000_000)); + const diff = process.hrtime.bigint() - request.startTime; + return Number(diff / BigInt(1_000_000)); }