docs: 基本完成初版数据库设计

This commit is contained in:
R524809
2026-10-15 10:09:34 +08:00
parent 63a6993276
commit e6ded00a4c
41 changed files with 2597 additions and 180 deletions
@@ -0,0 +1,69 @@
import {
ArgumentsHost,
Catch,
HttpException,
type ExceptionFilter,
} from '@nestjs/common';
import type { Response } from 'express';
import type { HttpRequest } from './http-request';
import { AppLogger } from '../logging/app-logger';
// 对外只提供稳定的安全提示;异常原始 message 可能包含请求值、路径或密钥。
const messages: Record<number, string> = {
400: '请求参数或格式不正确',
401: '请重新登录',
403: '没有访问权限',
404: '接口不存在',
409: '请求与当前数据冲突',
413: '请求体超过大小限制',
415: '不支持的请求格式',
429: '请求过于频繁',
503: '服务暂时不可用',
};
@Catch()
/** 将 Nest 和解析器异常转为统一 HTTP 错误,使用 requestId 关联服务端日志。 */
export class ApiErrorFilter implements ExceptionFilter {
constructor(private readonly logger: AppLogger) {}
catch(exception: unknown, host: ArgumentsHost): void {
const request = host.switchToHttp().getRequest<HttpRequest>();
const response = host.switchToHttp().getResponse<Response>();
// 未识别异常默认视为服务端错误;已知 HTTP 异常保留原状态码。
let status = 500;
if (exception instanceof HttpException) status = exception.getStatus();
else if (
exception &&
typeof exception === 'object' &&
'type' in exception
) {
// body-parser 在进入控制器前抛出的错误不一定是 Nest HttpException。
if (exception.type === 'entity.too.large') status = 413;
else if (exception.type === 'entity.parse.failed') status = 400;
else if (
exception.type === 'encoding.unsupported' ||
exception.type === 'charset.unsupported'
)
status = 415;
}
// 请求完成中间件记录全部状态;服务端错误另记诊断事件,不记录原请求及堆栈。
if (status >= 500) {
this.logger.error('http_request_failed', {
request_id: request.requestId,
method: request.method,
path: request.path,
status_code: status,
error_type:
exception instanceof Error
? exception.name
: 'UnknownError',
});
}
// HTTP 状态与响应 code 保持一致,客户端可用 requestId 反馈问题。
response.status(status).json({
code: status,
message: messages[status] ?? '服务器暂时无法处理请求',
requestId: request.requestId,
});
}
}
+14
View File
@@ -0,0 +1,14 @@
import type { Request } from 'express';
/** 守卫核对账户后写入的身份,后续业务不能用客户端提交的 userId 替代它。 */
export interface AuthenticatedUser {
userId: string;
authVersion: number;
role: 'user' | 'admin';
}
/** 生命周期内共享的请求上下文;未经过中间件或鉴权时字段可能尚未赋值。 */
export type HttpRequest = Request & {
requestId?: string;
auth?: AuthenticatedUser;
};
@@ -3,6 +3,7 @@ export interface RuntimeOptions {
port: number;
}
/** 解析监听地址与端口;默认仅本机可访问,部署时显式配置外部监听地址。 */
export function getRuntimeOptions(
env: NodeJS.ProcessEnv = process.env,
): RuntimeOptions {
@@ -10,6 +11,7 @@ export function getRuntimeOptions(
const port = Number(rawPort);
const host = (env.HOST ?? '127.0.0.1').trim();
// 先检查原始字符串,拒绝 Number 会接受的空串、科学计数法等输入。
if (
!/^\d+$/.test(rawPort) ||
!Number.isInteger(port) ||