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

+40
View File
@@ -0,0 +1,40 @@
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
REDIS_URL: z
.string()
.min(1)
.refine(
(value) => value.startsWith('redis://') || value.startsWith('rediss://'),
'REDIS_URL 必须是 redis:// 连接串',
),
QUEUE: z.string().min(1).default('default'),
WORKER_CONCURRENCY: z.coerce.number().int().min(1).max(64).default(2),
});
export interface WorkerConfig {
nodeEnv: 'development' | 'test' | 'production';
logLevel: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
redisUrl: string;
queue: string;
concurrency: number;
}
export function loadWorkerConfig(source: NodeJS.ProcessEnv = process.env): WorkerConfig {
const result = envSchema.safeParse(source);
if (!result.success) {
const details = result.error.issues
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
.join('; ');
throw new Error(`Worker 环境变量校验失败:${details}`);
}
return {
nodeEnv: result.data.NODE_ENV,
logLevel: result.data.LOG_LEVEL,
redisUrl: result.data.REDIS_URL,
queue: result.data.QUEUE,
concurrency: result.data.WORKER_CONCURRENCY,
};
}
+62
View File
@@ -0,0 +1,62 @@
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { config as loadDotenvFile } from 'dotenv';
import Redis from 'ioredis';
import pino from 'pino';
import { loadWorkerConfig } from './config';
function loadDotenv(): void {
for (const file of ['.env.local', '.env']) {
const path = resolve(process.cwd(), file);
if (existsSync(path)) {
loadDotenvFile({ path });
}
}
}
async function main(): Promise<void> {
loadDotenv();
const config = loadWorkerConfig();
const logger = pino({ level: config.logLevel, base: { service: 'lingxi-worker' } });
const redis = new Redis(config.redisUrl, {
lazyConnect: true,
maxRetriesPerRequest: null,
enableOfflineQueue: false,
});
redis.on('error', (error: Error) => {
logger.error({ err: error }, 'redis connection error');
});
await redis.connect();
await redis.ping();
logger.info({ queue: config.queue, concurrency: config.concurrency }, 'worker 已启动,等待任务');
// A0 只验证进程与依赖可用;BullMQ 消费者在 M1 批次接入(QUEUE 环境变量决定队列)
const heartbeat = setInterval(() => {
logger.debug({ queue: config.queue }, 'worker heartbeat');
}, 30_000);
heartbeat.unref();
const shutdown = async (signal: string): Promise<void> => {
clearInterval(heartbeat);
logger.info({ signal }, 'worker 正在退出');
await redis.quit();
};
process.on('SIGINT', () => {
void shutdown('SIGINT');
});
process.on('SIGTERM', () => {
void shutdown('SIGTERM');
});
}
void main().catch((error: unknown) => {
process.stderr.write(
`Worker 启动失败:${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
);
process.exitCode = 1;
});