feat(api): 搭建 API 与 Worker 骨架及契约基线

NestJS 11 + Fastify 统一错误体与 trace_id,/health 真实探活 DB/Redis,OpenAPI 3.1 契约含 20 个错误码;shared 包提供错误码、权限点、ULID、分页与通用 schema;worker 完成队列进程骨架。
This commit is contained in:
Stev_Wang committed 2026-11-02 17:20:46 +08:00
1 parent 09dbb27b66
commit 23067edb8f
70 files changed
+2445

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
/**
* 统一错误码(docs/工程规范/04-API与路由规范.md §4)
* API-14:新增错误码必须集中登记在这里,禁止业务代码写裸数字。
*/
export const ErrorCode = {
/** 400xx 参数校验 */
PARAM_INVALID: 40001,
/** 401xx 认证 */
UNAUTHENTICATED: 40101,
TOKEN_EXPIRED: 40102,
SIGNATURE_INVALID: 40103,
/** 403xx 授权 */
PERMISSION_DENIED: 40301,
OUT_OF_SCOPE: 40302,
SPECIAL_PERMISSION_REQUIRED: 40303,
SOD_VIOLATION: 40304,
/** 404xx 不存在 */
NOT_FOUND: 40401,
/** 409xx 冲突 */
VERSION_CONFLICT: 40901,
DUPLICATE_SUBMIT: 40902,
STATE_NOT_ALLOWED: 40903,
/** 422xx 业务规则 */
BUSINESS_RULE_VIOLATION: 42201,
COMPLIANCE_BLOCKED: 42202,
COST_OVER_BUDGET: 42203,
/** 429xx 限流 */
RATE_LIMITED: 42901,
CONCURRENCY_LIMITED: 42902,
/** 500xx 系统 */
INTERNAL_ERROR: 50001,
DEPENDENCY_UNAVAILABLE: 50002,
TIMEOUT: 50003,
} as const;
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
/** 错误码 → HTTP 状态码(4xx/5xx 段由百位决定) */
export const ERROR_HTTP_STATUS: Readonly<Record<ErrorCode, number>> = {
[ErrorCode.PARAM_INVALID]: 400,
[ErrorCode.UNAUTHENTICATED]: 401,
[ErrorCode.TOKEN_EXPIRED]: 401,
[ErrorCode.SIGNATURE_INVALID]: 401,
[ErrorCode.PERMISSION_DENIED]: 403,
[ErrorCode.OUT_OF_SCOPE]: 403,
[ErrorCode.SPECIAL_PERMISSION_REQUIRED]: 403,
[ErrorCode.SOD_VIOLATION]: 403,
[ErrorCode.NOT_FOUND]: 404,
[ErrorCode.VERSION_CONFLICT]: 409,
[ErrorCode.DUPLICATE_SUBMIT]: 409,
[ErrorCode.STATE_NOT_ALLOWED]: 409,
[ErrorCode.BUSINESS_RULE_VIOLATION]: 422,
[ErrorCode.COMPLIANCE_BLOCKED]: 422,
[ErrorCode.COST_OVER_BUDGET]: 422,
[ErrorCode.RATE_LIMITED]: 429,
[ErrorCode.CONCURRENCY_LIMITED]: 429,
[ErrorCode.INTERNAL_ERROR]: 500,
[ErrorCode.DEPENDENCY_UNAVAILABLE]: 503,
[ErrorCode.TIMEOUT]: 504,
};
/** 默认中文提示(前端兜底展示;服务端可按场景覆盖 message) */
export const ERROR_MESSAGES: Readonly<Record<ErrorCode, string>> = {
[ErrorCode.PARAM_INVALID]: '参数缺失或格式错误',
[ErrorCode.UNAUTHENTICATED]: '请先登录',
[ErrorCode.TOKEN_EXPIRED]: '登录已过期,请重新登录',
[ErrorCode.SIGNATURE_INVALID]: '签名校验失败',
[ErrorCode.PERMISSION_DENIED]: '没有操作权限',
[ErrorCode.OUT_OF_SCOPE]: '数据范围之外',
[ErrorCode.SPECIAL_PERMISSION_REQUIRED]: '缺少特殊权限',
[ErrorCode.SOD_VIOLATION]: '违反职责互斥,不能执行该操作',
[ErrorCode.NOT_FOUND]: '资源不存在或已删除',
[ErrorCode.VERSION_CONFLICT]: '数据已被他人修改,请刷新后重试',
[ErrorCode.DUPLICATE_SUBMIT]: '请勿重复提交',
[ErrorCode.STATE_NOT_ALLOWED]: '当前状态不允许该操作',
[ErrorCode.BUSINESS_RULE_VIOLATION]: '业务校验未通过',
[ErrorCode.COMPLIANCE_BLOCKED]: '内容触发合规拦截',
[ErrorCode.COST_OVER_BUDGET]: '成本超出预算',
[ErrorCode.RATE_LIMITED]: '请求过于频繁,请稍后再试',
[ErrorCode.CONCURRENCY_LIMITED]: '并发任务已达上限',
[ErrorCode.INTERNAL_ERROR]: '服务内部错误',
[ErrorCode.DEPENDENCY_UNAVAILABLE]: '依赖服务不可用',
[ErrorCode.TIMEOUT]: '请求超时',
};
const ERROR_CODE_VALUES: ReadonlySet<number> = new Set(Object.values(ErrorCode));
export function isErrorCode(value: number): value is ErrorCode {
return ERROR_CODE_VALUES.has(value);
}
/** 数字错误码对应的 HTTP 状态;优先查表(502/503/504 等细分),非法码回退 500 */
export function httpStatusOf(code: number): number {
if (isErrorCode(code)) {
return ERROR_HTTP_STATUS[code];
}
const segment = Math.floor(code / 100);
return segment >= 400 && segment <= 599 ? segment : 500;
}