- 建立 pnpm workspace + Turborepo 单仓、ESLint 9/Prettier/commitlint/husky 门禁 - NestJS + Fastify API:统一错误体、trace_id、Zod 管道、/health 真实依赖探活、OpenAPI 3.1 契约 - Prisma 基线:outbox_events/idempotency_keys/system_configs 与首版迁移、幂等种子 - shared 契约包:错误码、权限点、ULID、游标分页、命名/时间工具 - ui 包与 Web 壳层:Semi + Remix 图标统一出口、Cloud Blue 视觉、登录/工作台/错误页 - worker 骨架、Playwright 冒烟、Gitea CI、OpenAPI 快照与 oasdiff 回退脚本 - 补充 A0 实施报告与本地开发说明
This commit is contained in:
1 parent
9c3ab9768a
commit
04f49690f4
155 files changed
+18402
-28
No files matched your search
@@ -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 : '未知错误',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user