部署优化

This commit is contained in:
wchino
2026-06-13 22:16:14 +08:00
parent 21dd9957f0
commit 73a94e6be3
28 changed files with 899 additions and 129 deletions

View File

@@ -17,6 +17,7 @@ import { OperationsModule } from './domains/operations/operations.module';
import { AdminModule } from './applications/admin/admin.module';
import { PlayerModule } from './applications/player/player.module';
import { AgentPortalModule } from './applications/agent/agent-portal.module';
import { HealthModule } from './applications/health/health.module';
@Module({
imports: [
@@ -36,6 +37,7 @@ import { AgentPortalModule } from './applications/agent/agent-portal.module';
AdminModule,
PlayerModule,
AgentPortalModule,
HealthModule,
],
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
})

View File

@@ -0,0 +1,42 @@
import { ServiceUnavailableException } from '@nestjs/common';
import { HealthController } from './health.controller';
describe('HealthController', () => {
function createController(options?: { database?: boolean; redis?: boolean }) {
const database = options?.database ?? true;
const redis = options?.redis ?? true;
const prisma = {
$queryRaw: jest.fn().mockImplementation(() => {
if (!database) throw new Error('database unavailable');
return Promise.resolve([{ ok: 1 }]);
}),
};
const redisService = {
raw: {
ping: jest.fn().mockImplementation(() => {
if (!redis) throw new Error('redis unavailable');
return Promise.resolve('PONG');
}),
},
};
return new HealthController(prisma as never, redisService as never);
}
it('reports liveness without external checks', () => {
expect(createController().live()).toEqual({ status: 'ok' });
});
it('reports readiness when database and redis respond', async () => {
await expect(createController().ready()).resolves.toEqual({
status: 'ok',
checks: { database: 'ok', redis: 'ok' },
});
});
it('fails readiness when a dependency is unavailable', async () => {
await expect(createController({ redis: false }).ready()).rejects.toBeInstanceOf(
ServiceUnavailableException,
);
});
});

View File

@@ -0,0 +1,60 @@
import { Controller, Get, ServiceUnavailableException } from '@nestjs/common';
import { Public } from '../../shared/common/decorators';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { RedisService } from '../../shared/redis/redis.service';
type CheckStatus = 'ok' | 'error';
@Controller('health')
export class HealthController {
constructor(
private readonly prisma: PrismaService,
private readonly redis: RedisService,
) {}
@Public()
@Get('live')
live() {
return { status: 'ok' as const };
}
@Public()
@Get('ready')
async ready() {
const checks: Record<'database' | 'redis', CheckStatus> = {
database: 'ok',
redis: 'ok',
};
const [databaseReady, redisReady] = await Promise.all([
this.checkDatabase(),
this.checkRedis(),
]);
if (!databaseReady) checks.database = 'error';
if (!redisReady) checks.redis = 'error';
if (!databaseReady || !redisReady) {
throw new ServiceUnavailableException({ status: 'error', checks });
}
return { status: 'ok' as const, checks };
}
private async checkDatabase(): Promise<boolean> {
try {
await this.prisma.$queryRaw`SELECT 1`;
return true;
} catch {
return false;
}
}
private async checkRedis(): Promise<boolean> {
try {
return (await this.redis.raw.ping()) === 'PONG';
} catch {
return false;
}
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}