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

+56
View File
@@ -0,0 +1,56 @@
/**
* ULID 生成与解析(Crockford Base32,26 字符 = 48bit 时间 + 80bit 随机)
* 主键统一由应用侧生成 char(26) ULID(工程规范 05)。
*/
import { ULID_LENGTH } from './constants.js';
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
const ENCODING_LEN = 32;
export const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/;
function randomBytes(length: number): Uint8Array {
const bytes = new Uint8Array(length);
globalThis.crypto.getRandomValues(bytes);
return bytes;
}
/** 生成 ULID;同一毫秒内依靠 80bit 随机数避免碰撞 */
export function ulid(timestamp: number = Date.now()): string {
if (!Number.isInteger(timestamp) || timestamp < 0 || timestamp > 0xffffffffffff) {
throw new RangeError('ULID 时间戳必须是 48bit 非负整数');
}
let time = timestamp;
const timeChars: string[] = new Array<string>(10);
for (let i = 9; i >= 0; i -= 1) {
timeChars[i] = ENCODING[time % ENCODING_LEN] ?? '0';
time = Math.floor(time / ENCODING_LEN);
}
const randomChars = Array.from(randomBytes(16), (byte) => ENCODING[byte & 31] ?? '0');
return `${timeChars.join('')}${randomChars.join('')}`;
}
export function isUlid(value: string): boolean {
return value.length === ULID_LENGTH && ULID_PATTERN.test(value);
}
/** 解析 ULID 前 10 位得到毫秒时间戳 */
export function ulidToTimestamp(id: string): number {
if (!isUlid(id)) {
throw new TypeError(`不是合法 ULID:${id}`);
}
let timestamp = 0;
for (let i = 0; i < 10; i += 1) {
const char = id[i] ?? '';
const value = ENCODING.indexOf(char);
if (value < 0) {
throw new TypeError(`ULID 含非法字符:${char}`);
}
timestamp = timestamp * ENCODING_LEN + value;
}
return timestamp;
}
export function ulidToDate(id: string): Date {
return new Date(ulidToTimestamp(id));
}