import { createPool, type Pool } from 'mysql2/promise'; import type { OnModuleDestroy } from '@nestjs/common'; import type { MysqlRuntimeConfig } from '../../config/src/runtime-config'; import type { DependencyHealthDto } from '../../http-contracts/src/health.dto'; /** 固定的只读元数据查询,用于验证服务端版本、库名和会话字符口径。 */ export const MYSQL_HEALTH_QUERY = `SELECT VERSION() AS version, DATABASE() AS databaseName, @@character_set_database AS charset, @@collation_database AS collation, @@session.time_zone AS timezone`; interface MysqlQueryPort { query(sql: string): Promise<[unknown, unknown]>; end(): Promise; } interface MysqlMetadataRow { version: string; databaseName: string; charset: string; collation: string; timezone: string; } /** MySQL 健康客户端只校验元数据,不读取表数据或执行写入。 */ export class MysqlHealthClient implements OnModuleDestroy { constructor( private readonly client: MysqlQueryPort, private readonly expectedDatabase: string, ) {} async check(): Promise { const [rows] = await this.client.query(MYSQL_HEALTH_QUERY); const row = (rows as unknown as MysqlMetadataRow[])[0]; if (!row) throw new Error('MySQL 未返回元数据'); const versionMatch = /^(5\.7\.\d+)(?:[-+].*)?$/.exec(row.version ?? ''); if (!versionMatch) { throw new Error('MySQL 5.7.x 版本要求未满足'); } if (row.databaseName !== this.expectedDatabase) { throw new Error(`MySQL 健康检查必须连接 ${this.expectedDatabase}`); } if (row.charset !== 'utf8mb4') throw new Error('MySQL 字符集必须为 utf8mb4'); if (row.collation !== 'utf8mb4_general_ci') { throw new Error('MySQL 排序规则必须为 utf8mb4_general_ci'); } if (row.timezone !== '+08:00') throw new Error('MySQL 会话时区必须为 +08:00'); return { status: 'healthy', detail: `MySQL ${versionMatch[1]} / ${row.databaseName} / ${row.timezone}`, }; } async close(): Promise { await this.client.end(); } async onModuleDestroy(): Promise { await this.close(); } } /** 创建最多两个连接的只读健康池,并显式应用 MySQL 5.7.x 会话时区。 */ export function createMysqlHealthClient(config: MysqlRuntimeConfig): MysqlHealthClient { const pool: Pool = createPool({ host: config.host, port: config.port, database: config.database, user: config.username, password: config.password, charset: config.charset, timezone: config.timezone, connectionLimit: 2, enableKeepAlive: false, }); return new MysqlHealthClient(pool, config.database); }