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:
2026-05-29 22:27:56 +08:00
parent 34e915ccf4
commit 10b3928bee
26 changed files with 428 additions and 300 deletions

View 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,
});
}
};
}

View File

@@ -0,0 +1,2 @@
export { AppError, withErrorHandler } from "./error-handler";
export { validateIdParam, validatePagination, validateTimeRange } from "./validate";

View 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() };
}