feat(GOV-009): 建立后端契约与健康检查

This commit is contained in:
2026-08-13 17:26:51 +08:00
parent 3fc703de0b
commit 420aa22342
32 changed files with 1625 additions and 8 deletions
+26 -1
View File
@@ -1,4 +1,29 @@
import { Module } from '@nestjs/common';
@Module({})
import { loadApiProcessConfig } from '../../../libs/config/src/load-process-env';
import { DisabledHealthClient } from '../../../libs/infrastructure/src/disabled-health.client';
import { createMysqlHealthClient } from '../../../libs/infrastructure/src/mysql-health.client';
import { createRedisHealthClient } from '../../../libs/infrastructure/src/redis-health.client';
import { HealthController } from './health.controller';
import { HealthService, MYSQL_HEALTH_PORT, REDIS_HEALTH_PORT } from './health.service';
const apiConfig = loadApiProcessConfig();
@Module({
controllers: [HealthController],
providers: [
HealthService,
{
provide: REDIS_HEALTH_PORT,
useFactory: () => createRedisHealthClient(apiConfig.redis),
},
{
provide: MYSQL_HEALTH_PORT,
useFactory: () =>
apiConfig.mysql.enabled
? createMysqlHealthClient(apiConfig.mysql)
: new DisabledHealthClient('MySQL 未启用'),
},
],
})
export class ApiModule {}
+22
View File
@@ -0,0 +1,22 @@
import { Controller, Get, HttpException, HttpStatus, Inject } from '@nestjs/common';
import { ApiOkResponse, ApiServiceUnavailableResponse, ApiTags } from '@nestjs/swagger';
import { HealthResponseDto } from '../../../libs/http-contracts/src/health.dto';
import { HealthService } from './health.service';
@ApiTags('health')
@Controller('health')
export class HealthController {
constructor(@Inject(HealthService) private readonly healthService: HealthService) {}
@Get()
@ApiOkResponse({ type: HealthResponseDto })
@ApiServiceUnavailableResponse({ type: HealthResponseDto })
async getHealth(): Promise<HealthResponseDto> {
const response = await this.healthService.check();
if (response.status === 'unavailable') {
throw new HttpException(response, HttpStatus.SERVICE_UNAVAILABLE);
}
return response;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { Inject, Injectable } from '@nestjs/common';
import type {
DependencyHealthDto,
HealthResponseDto,
} from '../../../libs/http-contracts/src/health.dto';
export const REDIS_HEALTH_PORT = Symbol('REDIS_HEALTH_PORT');
export const MYSQL_HEALTH_PORT = Symbol('MYSQL_HEALTH_PORT');
export interface DependencyHealthPort {
check(): Promise<DependencyHealthDto>;
}
@Injectable()
export class HealthService {
constructor(
@Inject(REDIS_HEALTH_PORT) private readonly redis: DependencyHealthPort,
@Inject(MYSQL_HEALTH_PORT) private readonly mysql: DependencyHealthPort,
) {}
async check(): Promise<HealthResponseDto> {
const [redis, mysql] = await Promise.all([
this.checkSafely(this.redis),
this.checkSafely(this.mysql),
]);
const status =
redis.status === 'healthy' && mysql.status !== 'unavailable' ? 'healthy' : 'unavailable';
return {
status,
process: 'api',
checkedAt: new Date().toISOString(),
dependencies: {
redis,
mysql,
rocketMq: { status: 'disabled', detail: 'GOV-009 尚未启用 RocketMQ 客户端' },
},
};
}
private async checkSafely(port: DependencyHealthPort): Promise<DependencyHealthDto> {
try {
const result = await port.check();
if (result.status === 'healthy' || result.status === 'disabled') return result;
} catch {
// 外部错误可能包含 DSN、用户名或密码;HTTP 健康响应只暴露固定安全文案。
}
return { status: 'unavailable', detail: '依赖检查失败' };
}
}
+1
View File
@@ -18,6 +18,7 @@ interface BootstrapApiOptions {
export async function bootstrapApi(options: BootstrapApiOptions): Promise<INestApplication> {
const app = await NestFactory.create(ApiModule, { logger: options.logger });
app.setGlobalPrefix('api/v1');
app.enableShutdownHooks();
await app.listen(options.port ?? options.config.api.port, options.config.api.host);
return app;