feat: WC2026 赛事 seed、生产上线初始化脚本与目录归档
重构 seed 为 WC2026 72 场小组赛与 48 强优胜盘;新增 production 模式仅保留 admin 与赛事示例;提供 prod-init-db 全量重置脚本;管理端 i18n 分包与赛事归档能力。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,6 +31,10 @@ export function appForbidden(code: ApiErrorCode, params?: ApiErrorParams) {
|
||||
return new ForbiddenException(body(code, params));
|
||||
}
|
||||
|
||||
export function appConflict(code: ApiErrorCode, data?: unknown, params?: ApiErrorParams) {
|
||||
return new HttpException({ ...body(code, params), data: data ?? null }, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
export function appUnauthorized(code: ApiErrorCode, params?: ApiErrorParams) {
|
||||
return new UnauthorizedException(body(code, params));
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let code: ApiErrorCode = 'INTERNAL_SERVER_ERROR';
|
||||
let params: ApiErrorParams | undefined;
|
||||
let extraData: unknown = null;
|
||||
let message = formatApiErrorMessage('INTERNAL_SERVER_ERROR', locale);
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
@@ -37,6 +38,9 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
code = res.code;
|
||||
params = res.params;
|
||||
message = formatApiErrorMessage(code, locale, params);
|
||||
if (typeof res === 'object' && res !== null && 'data' in res) {
|
||||
extraData = (res as { data?: unknown }).data ?? null;
|
||||
}
|
||||
} else if (typeof res === 'string') {
|
||||
message = res;
|
||||
} else if (typeof res === 'object' && res !== null) {
|
||||
@@ -60,7 +64,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
error: message,
|
||||
code,
|
||||
params: params ?? null,
|
||||
data: null,
|
||||
data: extraData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
18
apps/api/src/shared/uploads/delete-upload-file.ts
Normal file
18
apps/api/src/shared/uploads/delete-upload-file.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { unlink } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { getUploadRoot } from './upload-paths';
|
||||
|
||||
const UPLOAD_URL_PREFIX = '/uploads/';
|
||||
|
||||
/** 按 `/uploads/{category}/{filename}` 删除磁盘文件;路径非法或文件不存在时静默跳过 */
|
||||
export async function deleteUploadFileByUrl(url: string): Promise<void> {
|
||||
if (!url?.startsWith(UPLOAD_URL_PREFIX)) return;
|
||||
const relative = url.slice(UPLOAD_URL_PREFIX.length);
|
||||
if (!relative || relative.includes('..') || relative.includes('\\')) return;
|
||||
const root = getUploadRoot();
|
||||
try {
|
||||
await unlink(join(root, relative));
|
||||
} catch {
|
||||
/* already removed */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user