部署优化
This commit is contained in:
@@ -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 }],
|
||||
})
|
||||
|
||||
42
apps/api/src/applications/health/health.controller.spec.ts
Normal file
42
apps/api/src/applications/health/health.controller.spec.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
60
apps/api/src/applications/health/health.controller.ts
Normal file
60
apps/api/src/applications/health/health.controller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
apps/api/src/applications/health/health.module.ts
Normal file
7
apps/api/src/applications/health/health.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class HealthModule {}
|
||||
@@ -95,6 +95,7 @@ function selectTab(tab: SlipMode) {
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
slip.closeDrawer();
|
||||
show.value = false;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
@@ -305,7 +306,7 @@ watch(
|
||||
<p class="drawer-kicker">{{ t('bet.bet_slip') }}</p>
|
||||
<h3>{{ t('bet.slip_review_title') }}</h3>
|
||||
</div>
|
||||
<button type="button" class="close-btn" :aria-label="t('bet.cancel')" @click="closeDrawer">
|
||||
<button type="button" class="close-btn" :aria-label="t('bet.cancel')" @click.stop="closeDrawer">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@@ -473,6 +474,9 @@ watch(
|
||||
}
|
||||
|
||||
.drawer-head {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
@@ -497,6 +501,9 @@ watch(
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
@@ -504,6 +511,8 @@ watch(
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
padding: 0;
|
||||
pointer-events: auto;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.balance-bar {
|
||||
|
||||
Reference in New Issue
Block a user