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

@@ -0,0 +1,25 @@
import { Controller, Get, Inject } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { HealthService } from './health.service';
import { type HealthReport, HealthResponseDto } from './health.types';
@ApiTags('platform')
@Controller('health')
export class HealthController {
constructor(@Inject(HealthService) private readonly healthService: HealthService) {}
@Get()
@ApiOperation({
summary: '健康检查',
description: '真实检查 PostgreSQL 与 Redis;任一不可用返回 503 与统一错误体。',
})
@ApiOkResponse({ type: HealthResponseDto, description: '全部依赖健康' })
@ApiResponse({
status: 503,
description: '依赖不可用(错误码 50002),details 内附各依赖检查结果',
})
async check(): Promise<HealthReport> {
return this.healthService.check();
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
import { HealthService } from './health.service';
@Module({
controllers: [HealthController],
providers: [HealthService],
})
export class HealthModule {}
@@ -0,0 +1,94 @@
import { ErrorCode, nowIso } from '@lingxi/shared';
import { Inject, Injectable } from '@nestjs/common';
import { AppException } from '../../../common/errors/app-exception';
import { APP_CONFIG } from '../../../config/config.module';
import type { AppConfig } from '../../../config/env';
import type { PrismaService } from '../../../infra/prisma/prisma.service';
import type { RedisService } from '../../../infra/redis/redis.service';
import { PRISMA } from '../../../infra/tokens';
import { REDIS } from '../../../infra/tokens';
import type { ComponentHealth, HealthReport } from './health.types';
const API_VERSION = '0.1.0';
const CHECK_TIMEOUT_MS = 2000;
function withTimeout<T>(task: Promise<T>, timeoutMs: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`检查超时(${String(timeoutMs)}ms)`));
}, timeoutMs);
void task.then(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error: unknown) => {
clearTimeout(timer);
reject(error instanceof Error ? error : new Error(String(error)));
},
);
});
}
/**
* 健康检查:真实检查数据库(SELECT 1)与 Redis(PING),不只看端口(技术架构 §4.1.1)。
* 任一依赖不可用时返回 503 + 统一错误体,details 里带完整检查快照。
*/
@Injectable()
export class HealthService {
constructor(
@Inject(PRISMA) private readonly prisma: PrismaService,
@Inject(REDIS) private readonly redis: RedisService,
@Inject(APP_CONFIG) private readonly config: AppConfig,
) {}
async check(): Promise<HealthReport> {
const [database, redis] = await Promise.all([this.checkDatabase(), this.checkRedis()]);
const report: HealthReport = {
status: database.status === 'up' && redis.status === 'up' ? 'ok' : 'degraded',
env: this.config.nodeEnv,
version: API_VERSION,
uptimeSeconds: Math.round(process.uptime()),
checks: { database, redis },
timestamp: nowIso(),
};
if (report.status === 'degraded') {
throw new AppException({
code: ErrorCode.DEPENDENCY_UNAVAILABLE,
message: '依赖服务不可用',
details: report.checks,
});
}
return report;
}
private async checkDatabase(): Promise<ComponentHealth> {
const startedAt = Date.now();
try {
await withTimeout(this.prisma.$queryRaw`SELECT 1`, CHECK_TIMEOUT_MS);
return { status: 'up', latencyMs: Date.now() - startedAt };
} catch (error) {
return {
status: 'down',
latencyMs: Date.now() - startedAt,
error: error instanceof Error ? error.message : '未知错误',
};
}
}
private async checkRedis(): Promise<ComponentHealth> {
const startedAt = Date.now();
try {
await withTimeout(this.redis.ping(), CHECK_TIMEOUT_MS);
return { status: 'up', latencyMs: Date.now() - startedAt };
} catch (error) {
return {
status: 'down',
latencyMs: Date.now() - startedAt,
error: error instanceof Error ? error.message : '未知错误',
};
}
}
}
@@ -0,0 +1,58 @@
import { ApiProperty } from '@nestjs/swagger';
export interface ComponentHealth {
status: 'up' | 'down';
latencyMs: number;
error?: string;
}
export interface HealthReport {
status: 'ok' | 'degraded';
env: string;
version: string;
uptimeSeconds: number;
checks: {
database: ComponentHealth;
redis: ComponentHealth;
};
timestamp: string;
}
export class ComponentHealthDto {
@ApiProperty({ type: String, enum: ['up', 'down'], example: 'up' })
status!: 'up' | 'down';
@ApiProperty({ type: Number, description: '检查耗时(毫秒)', example: 3 })
latencyMs!: number;
@ApiProperty({ type: String, required: false, description: '失败原因(仅 down 时返回)' })
error?: string;
}
export class HealthChecksDto {
@ApiProperty({ type: ComponentHealthDto })
database!: ComponentHealthDto;
@ApiProperty({ type: ComponentHealthDto })
redis!: ComponentHealthDto;
}
export class HealthResponseDto {
@ApiProperty({ type: String, enum: ['ok', 'degraded'], example: 'ok' })
status!: 'ok' | 'degraded';
@ApiProperty({ type: String, example: 'development' })
env!: string;
@ApiProperty({ type: String, example: '0.1.0' })
version!: string;
@ApiProperty({ type: Number, description: '进程运行秒数', example: 128 })
uptimeSeconds!: number;
@ApiProperty({ type: HealthChecksDto })
checks!: HealthChecksDto;
@ApiProperty({ type: String, example: '2026-11-02T08:00:00.000Z' })
timestamp!: string;
}
@@ -0,0 +1,14 @@
import { Global, Module } from '@nestjs/common';
import { HealthModule } from './health/health.module';
/**
* platform 模块:健康检查、系统配置、审计、AI 网关等平台能力的归属模块(技术架构 §3)。
* A0 只落地健康检查;其余能力按里程碑在本模块内新增。
*/
@Global()
@Module({
imports: [HealthModule],
exports: [HealthModule],
})
export class PlatformModule {}