refactor: 代码审查修复 — 错误边界、DRY抽取、测试修复、合规性改进
- P1: server.ts 统一错误边界 (withErrorHandler + AppError),修复 3 个失败/卡死测试 - P2: db 层 wrap/paginateQuery 抽取,前端 handleResponse 抽取,parseIdFromUrl 抽取 - P3: middleware 验证消息中文化,Flex→Space 替换 - P0: docs/development/README.md 新增已知设计决策章节 - P3-11 setup 拆分已尝试回退(@testing-library/react preload 依赖无法拆分) - P3-13 config 层测试从本次变更移除
This commit is contained in:
37
src/server/middleware/error-handler.ts
Normal file
37
src/server/middleware/error-handler.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { RuntimeMode } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { createApiError, jsonResponse } from "../helpers";
|
||||
|
||||
type RouteHandler = (req: Request) => Promise<Response> | Response;
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly statusCode: number,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "AppError";
|
||||
}
|
||||
}
|
||||
|
||||
export function withErrorHandler(fn: RouteHandler, mode: RuntimeMode, logger?: Logger): RouteHandler {
|
||||
return async (req) => {
|
||||
try {
|
||||
return await fn(req);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AppError) {
|
||||
return jsonResponse(createApiError(error.message, error.statusCode), {
|
||||
mode,
|
||||
status: error.statusCode,
|
||||
});
|
||||
}
|
||||
|
||||
logger?.error({ error }, "未处理的路由异常");
|
||||
return jsonResponse(createApiError("服务器内部错误", 500), {
|
||||
mode,
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
2
src/server/middleware/index.ts
Normal file
2
src/server/middleware/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AppError, withErrorHandler } from "./error-handler";
|
||||
export { validateIdParam, validatePagination, validateTimeRange } from "./validate";
|
||||
63
src/server/middleware/validate.ts
Normal file
63
src/server/middleware/validate.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { RuntimeMode } from "../../shared/api";
|
||||
|
||||
import { createApiError, jsonResponse } from "../helpers";
|
||||
|
||||
const MAX_PAGE_SIZE = 200;
|
||||
|
||||
export function validateIdParam(idStr: string, mode: RuntimeMode): Response | { id: string } {
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(idStr)) {
|
||||
return jsonResponse(createApiError("无效的 ID 参数", 400), { mode, status: 400 });
|
||||
}
|
||||
return { id: idStr };
|
||||
}
|
||||
|
||||
export function validatePagination(
|
||||
pageParam: null | string,
|
||||
pageSizeParam: null | string,
|
||||
mode: RuntimeMode,
|
||||
): Response | { page: number; pageSize: number } {
|
||||
let page = 1;
|
||||
let pageSize = 20;
|
||||
|
||||
if (pageParam !== null) {
|
||||
page = Number(pageParam);
|
||||
if (!Number.isInteger(page) || page <= 0) {
|
||||
return jsonResponse(createApiError("无效的 page 参数", 400), { mode, status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
if (pageSizeParam !== null) {
|
||||
pageSize = Number(pageSizeParam);
|
||||
if (!Number.isInteger(pageSize) || pageSize <= 0) {
|
||||
return jsonResponse(createApiError("无效的 pageSize 参数", 400), { mode, status: 400 });
|
||||
}
|
||||
if (pageSize > MAX_PAGE_SIZE) {
|
||||
return jsonResponse(createApiError(`pageSize 不能超过 ${MAX_PAGE_SIZE}`, 400), { mode, status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
return { page, pageSize };
|
||||
}
|
||||
|
||||
export function validateTimeRange(
|
||||
from: null | string,
|
||||
to: null | string,
|
||||
mode: RuntimeMode,
|
||||
): Response | { from: string; to: string } {
|
||||
if (!from || !to) {
|
||||
return jsonResponse(createApiError("from 和 to 参数为必填项", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
const fromDate = new Date(from);
|
||||
const toDate = new Date(to);
|
||||
|
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
|
||||
return jsonResponse(createApiError("无效的 from 或 to 参数格式", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (fromDate.getTime() > toDate.getTime()) {
|
||||
return jsonResponse(createApiError("from 必须早于 to", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
return { from: fromDate.toISOString(), to: toDate.toISOString() };
|
||||
}
|
||||
Reference in New Issue
Block a user