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:
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 {}
|
||||
Reference in new issue
Block a user