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(task: Promise, timeoutMs: number): Promise { return new Promise((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 { 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 { 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 { 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 : '未知错误', }; } } }