// db.js - MySQL 连接层(mysql2 连接池)+ schema 装载 // // 配置来源优先级(高 → 低): // 1. 调用方显式传入的配置对象 // 2. 环境变量 TANK_DB_HOST / TANK_DB_USER / TANK_DB_PASSWORD / TANK_DB_PORT / TANK_DB_NAME // 3. local/db.json(未跟踪目录,凭据不进 git) // 4. 内置默认(127.0.0.1:3306 / root / 库名 tanki) // // 关键取数口径:协议里的 id 是 i64,且**确实存在负数**(-2147481788 一族合成对象 id), // 所以这里必须 supportBigNumbers + bigNumberStrings —— 大整数一律以**十进制字符串**返回, // 与 tools/ 里既有代码「id 全部当字符串比较」的口径一致(改用 Number 会在大 id 上静默丢精度)。 'use strict'; const fs = require('fs'); const path = require('path'); const PROJECT = path.resolve(__dirname, '..', '..'); const CONFIG_PATH = path.join(PROJECT, 'local', 'db.json'); const SCHEMA_PATH = path.join(__dirname, 'schema.sql'); const DEFAULTS = { Host: '127.0.0.1', User: 'root', Password: '', Port: 3306, Database: 'tanki', }; function ReadConfigFile(File) { const P = File || CONFIG_PATH; if (!fs.existsSync(P)) return {}; try { return JSON.parse(fs.readFileSync(P, 'utf8')); } catch (Err) { throw new Error('读不到数据库配置 ' + P + ':' + Err.message); } } // 合出最终配置;缺项一律落到 DEFAULTS,不做隐式猜测 function LoadConfig(Explicit) { const FromFile = ReadConfigFile(Explicit && Explicit.ConfigFile); const Env = { Host: process.env.TANK_DB_HOST, User: process.env.TANK_DB_USER, Password: process.env.TANK_DB_PASSWORD, Port: process.env.TANK_DB_PORT ? Number(process.env.TANK_DB_PORT) : undefined, Database: process.env.TANK_DB_NAME, }; const Pick = (Key) => { const Sources = [Explicit && Explicit[Key], Env[Key], FromFile[Key], DEFAULTS[Key]]; for (const V of Sources) if (V !== undefined && V !== null && V !== '') return V; return DEFAULTS[Key]; }; return { Host: String(Pick('Host')), User: String(Pick('User')), Password: String(Pick('Password')), Port: Number(Pick('Port')) || DEFAULTS.Port, Database: String(Pick('Database')), }; } // 建连接池。mysql2 是可选依赖:没装时给出可操作的报错,而不是一句 module not found function Connect(Config) { let mysql; try { mysql = require('mysql2/promise'); } catch (Err) { throw new Error('没装 mysql2(在 tools/ 下执行 npm install mysql2)'); } const C = Config || LoadConfig(); return mysql.createPool({ host: C.Host, user: C.User, password: C.Password, port: C.Port, database: C.Database, waitForConnections: true, connectionLimit: 8, charset: 'utf8mb4', supportBigNumbers: true, bigNumberStrings: true, multipleStatements: true, // EnsureSchema 要整份 schema.sql 一次执行 }); } // 把 schema.sql 跑一遍(幂等:全部 CREATE TABLE IF NOT EXISTS) async function EnsureSchema(Pool, SchemaFile) { const Sql = fs.readFileSync(SchemaFile || SCHEMA_PATH, 'utf8'); await Pool.query(Sql); const [Rows] = await Pool.query( 'SELECT TABLE_NAME AS Name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME', ); return Rows.map((R) => R.Name); } // 取一行(没有则 null) async function One(Pool, Sql, Params) { const [Rows] = await Pool.query(Sql, Params || []); return Rows.length ? Rows[0] : null; } // 取多行 async function Many(Pool, Sql, Params) { const [Rows] = await Pool.query(Sql, Params || []); return Rows; } // 批量插入(Values 是数组的数组),自动按列切片拼多条 VALUES async function InsertMany(Pool, Table, Columns, Values, ChunkSize) { if (!Values.length) return 0; const Size = ChunkSize || 500; const Cols = Columns.map((C) => '`' + C + '`').join(', '); let Done = 0; for (let I = 0; I < Values.length; I += Size) { const Chunk = Values.slice(I, I + Size); const Placeholders = Chunk.map(() => '(' + Columns.map(() => '?').join(', ') + ')').join(', '); const Flat = []; for (const Row of Chunk) for (const V of Row) Flat.push(V); await Pool.query('INSERT INTO `' + Table + '` (' + Cols + ') VALUES ' + Placeholders, Flat); Done += Chunk.length; } return Done; } module.exports = { LoadConfig, Connect, EnsureSchema, One, Many, InsertMany, CONFIG_PATH, SCHEMA_PATH, DEFAULTS };