Files
trade/data/config.ts
T
Rekey e91cad79e6 feat(data): 实现配置表 CRUD 与 Schema 初始化拆分
- 新增 data/db/ 数据库访问层:pool 管理、类型定义、Zod 校验、参数化 SQL 查询
- 新增 data/db/config-crud.ts:MonitoredSymbolsRepo / ExchangeConfigRepo / AppConfigRepo 三个 CRUD 服务类
- 新增 data/config.ts:中心化配置模块,零依赖 .env 解析 + Zod 校验
- 新增 data/schema/:klines.sql + config.sql 参考 DDL
- 新增 data/exchanges/:交易所类型定义与 Binance WebSocket 封装
- 新增 data/run/:交易所连接启动入口
- 重构 data/init-db/:001_init.sql 仅保留 TimescaleDB + klines,配置表拆分至 002_config.sql
- 更新 docker-compose.yml:挂载 init-db 初始化脚本
2026-06-07 20:46:35 +08:00

185 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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<string, string> {
const result: Record<string, string> = {};
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 连接 URLioredis 可直接使用) */
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));
}