// ============================================================ // config.ts — 中心化配置模块(带 Zod 运行时校验) // ============================================================ // 职责: // 1. 从 .env 文件加载环境变量(零依赖,手动解析) // 2. 使用 EnvConfigSchema 校验并类型收窄 // 3. 导出按职责分组的强类型配置对象(pgsql / redis / batch / ws / logging / symbols) // // 使用方式: // import { pgsql, redis, batch, ws, logging, symbols } from "./config"; // const pool = new pg.Pool(pgsql); // const redisClient = new Redis(redis.url); // ============================================================ import { readFileSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { EnvConfigSchema, type EnvConfig } from "./db/validators"; // ============================================================ // 1. 加载 .env 文件(零依赖实现) // ============================================================ /** * 手动解析 .env 文件为 key-value 对。 * 规则: * - 忽略空行和 # 开头的注释行 * - 首个 = 分割 key=value * - 去除首尾空白(不处理引号,值原样保留) * - 跳过不含 = 的行 * * 不依赖 dotenv 包,保持依赖精简。 */ function parseEnvFile(filePath: string): Record { const result: Record = {}; try { const content = readFileSync(filePath, "utf-8"); for (const line of content.split("\n")) { const trimmed = line.trim(); // 跳过空行和注释 if (trimmed === "" || trimmed.startsWith("#")) { continue; } const eqIdx = trimmed.indexOf("="); if (eqIdx === -1) { continue; } const key = trimmed.slice(0, eqIdx).trim(); const value = trimmed.slice(eqIdx + 1).trim(); if (key !== "") { result[key] = value; } } } catch { // .env 文件不存在时不报错(生产环境变量由容器注入) } return result; } /** * 将解析结果注入 process.env(已存在的变量不覆盖)。 * 这使得后续 Zod 的 `z.coerce` 可以从 process.env 读取。 */ function loadEnvFile(): void { // __dirname 在 ESM 中不可用,手动计算 const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const envPath = resolve(__dirname, ".env"); const parsed = parseEnvFile(envPath); for (const [key, value] of Object.entries(parsed)) { // 不覆盖已由系统设置的环境变量(Docker / systemd 注入优先) if (process.env[key] === undefined) { process.env[key] = value; } } } // 模块加载时立即执行 loadEnvFile(); // ============================================================ // 2. Zod 校验 & 类型收窄 // ============================================================ /** * 经 Zod 校验后的环境变量配置。 * 所有字段均有默认值兜底,即使 .env 缺失也能正常运行。 */ const rawConfig: EnvConfig = EnvConfigSchema.parse(process.env); // ============================================================ // 3. 按职责分组的导出配置对象 // ============================================================ /** PostgreSQL / TimescaleDB 连接配置 */ export const pgsql = { host: rawConfig.DB_HOST, port: rawConfig.DB_PORT, database: rawConfig.DB_NAME, user: rawConfig.DB_USER, password: rawConfig.DB_PASSWORD, /** pg.Pool 连接上限,避免连接数暴涨 */ max: 20, /** 连接空闲超时(毫秒) */ idleTimeoutMillis: 30000, /** 连接获取超时(毫秒) */ connectionTimeoutMillis: 5000, } as const; /** Redis 连接与发布配置 */ export const redis = { /** Redis 连接 URL(ioredis 可直接使用) */ url: rawConfig.REDIS_URL, /** 是否启用 Pub/Sub 发布行情数据(开发环境可关闭以节省资源) */ publishEnabled: rawConfig.REDIS_PUBLISH_ENABLED === "true", /** 频道前缀,避免多环境 key 冲突 */ channelPrefix: "trade", /** 重连策略:指数退避基数(毫秒) */ retryDelayBaseMs: 1000, /** 最大重试次数 */ maxRetries: 10, } as const; /** K 线批量写入配置 */ export const batch = { /** 缓冲区条数阈值(达到后自动刷新) */ size: rawConfig.BATCH_SIZE, /** 最大缓冲时间(毫秒),超时后即使未达阈值也刷新 */ flushIntervalMs: rawConfig.FLUSH_INTERVAL_MS, } as const; /** WebSocket 连接配置(全局默认值,交易所级别可覆盖) */ export const ws = { /** 断线重连延迟基数(毫秒),指数退避:基数 × 2^attempts */ reconnectDelayMs: rawConfig.WS_RECONNECT_DELAY_MS, /** 心跳间隔(毫秒) */ pingIntervalMs: rawConfig.WS_PING_INTERVAL_MS, /** 最大重连次数(超过后标记 error 状态) */ maxReconnectAttempts: rawConfig.WS_MAX_RECONNECT_ATTEMPTS, } as const; /** 日志配置 */ export const logging = { /** 日志级别:trace / debug / info / warn / error / fatal */ level: rawConfig.LOG_LEVEL, /** 运行环境(production 时 pino 输出 JSON 便于日志采集) */ nodeEnv: rawConfig.NODE_ENV, /** 是否启用 pino-pretty(开发环境友好输出) */ pretty: rawConfig.NODE_ENV === "development", } as const; /** 默认订阅的交易对列表(逗号分隔 → string[]) */ export const symbols: string[] = rawConfig.SYMBOLS.split(",") .map((s) => s.trim()) .filter(Boolean); // ============================================================ // 4. 工具:运行时打印配置概要(不含敏感信息) // ============================================================ /** 打印脱敏后的配置概要,便于启动排查 */ export function printConfigSummary(): void { const summary = { pgsql: { host: pgsql.host, port: pgsql.port, database: pgsql.database, user: pgsql.user, password: "***", }, redis: { url: redis.url.replace(/\/\/.*@/, "//***@"), // 隐藏密码 publishEnabled: redis.publishEnabled, }, batch, ws, logging, symbols, }; console.log("[config] 配置概要:", JSON.stringify(summary, null, 2)); }