部署优化

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 {}

View File

@@ -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 {