2026-05-23 14:15:45 +08:00
|
|
|
const mysql = require('mysql2/promise')
|
2026-05-26 09:18:48 +08:00
|
|
|
require('dotenv').config()
|
2026-05-23 14:15:45 +08:00
|
|
|
|
2026-05-26 09:30:17 +08:00
|
|
|
function requireEnv(name, fallback) {
|
|
|
|
|
const value = process.env[name] || fallback
|
|
|
|
|
if (!value && !fallback) {
|
|
|
|
|
throw new Error(`Missing required environment variable: ${name}. Check .env file.`)
|
|
|
|
|
}
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
const config = {
|
2026-05-26 09:30:17 +08:00
|
|
|
host: requireEnv('DB_HOST', 'localhost'),
|
|
|
|
|
port: parseInt(requireEnv('DB_PORT', '3306')),
|
|
|
|
|
user: requireEnv('DB_USER', 'root'),
|
|
|
|
|
password: requireEnv('DB_PASSWORD', ''),
|
|
|
|
|
database: requireEnv('DB_NAME', 'miniprogram'),
|
2026-05-23 14:15:45 +08:00
|
|
|
waitForConnections: true,
|
2026-06-03 14:15:55 +08:00
|
|
|
connectionLimit: parseInt(process.env.DB_POOL_LIMIT) || 10,
|
2026-05-23 14:15:45 +08:00
|
|
|
queueLimit: 0
|
|
|
|
|
}
|
2026-05-26 09:30:17 +08:00
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
const SLOW_QUERY_MS = parseInt(process.env.SLOW_QUERY_MS) || 500
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
const pool = mysql.createPool(config)
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
const queryStats = {
|
|
|
|
|
total: 0,
|
|
|
|
|
slow: 0,
|
|
|
|
|
errors: 0,
|
|
|
|
|
totalDurationMs: 0,
|
|
|
|
|
lastSlow: []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function recordSlow(sql, durationMs, params) {
|
|
|
|
|
queryStats.slow += 1
|
|
|
|
|
const entry = {
|
|
|
|
|
sql: sql.replace(/\s+/g, ' ').slice(0, 200),
|
|
|
|
|
durationMs: Math.round(durationMs),
|
|
|
|
|
paramCount: Array.isArray(params) ? params.length : 0,
|
|
|
|
|
at: new Date().toISOString()
|
|
|
|
|
}
|
|
|
|
|
queryStats.lastSlow.unshift(entry)
|
|
|
|
|
if (queryStats.lastSlow.length > 20) queryStats.lastSlow.pop()
|
|
|
|
|
console.warn('[SLOW QUERY]', entry.durationMs + 'ms', entry.sql)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
async function query(sql, params = []) {
|
2026-06-03 14:15:55 +08:00
|
|
|
const start = Date.now()
|
2026-05-23 14:15:45 +08:00
|
|
|
const connection = await pool.getConnection()
|
|
|
|
|
try {
|
2026-06-03 14:15:55 +08:00
|
|
|
const [rows] = await connection.execute(sql, params)
|
|
|
|
|
const duration = Date.now() - start
|
|
|
|
|
queryStats.total += 1
|
|
|
|
|
queryStats.totalDurationMs += duration
|
|
|
|
|
if (duration >= SLOW_QUERY_MS) recordSlow(sql, duration, params)
|
2026-05-23 14:15:45 +08:00
|
|
|
return rows
|
2026-06-03 14:15:55 +08:00
|
|
|
} catch (err) {
|
|
|
|
|
queryStats.errors += 1
|
|
|
|
|
throw err
|
2026-05-23 14:15:45 +08:00
|
|
|
} finally {
|
|
|
|
|
connection.release()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
function getPoolMetrics() {
|
|
|
|
|
const p = pool.pool || pool
|
|
|
|
|
return {
|
|
|
|
|
connectionLimit: config.connectionLimit,
|
|
|
|
|
allConnections: (p._allConnections && p._allConnections.length) || 0,
|
|
|
|
|
freeConnections: (p._freeConnections && p._freeConnections.length) || 0,
|
|
|
|
|
acquiringConnections: (p._acquiringConnections && p._acquiringConnections.length) || 0,
|
|
|
|
|
queue: (p._connectionQueue && p._connectionQueue.length) || 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getQueryStats() {
|
|
|
|
|
const avg = queryStats.total > 0 ? Math.round(queryStats.totalDurationMs / queryStats.total) : 0
|
|
|
|
|
return {
|
|
|
|
|
total: queryStats.total,
|
|
|
|
|
slow: queryStats.slow,
|
|
|
|
|
errors: queryStats.errors,
|
|
|
|
|
avgDurationMs: avg,
|
|
|
|
|
lastSlow: queryStats.lastSlow.slice(0, 10)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetQueryStats() {
|
|
|
|
|
queryStats.total = 0
|
|
|
|
|
queryStats.slow = 0
|
|
|
|
|
queryStats.errors = 0
|
|
|
|
|
queryStats.totalDurationMs = 0
|
|
|
|
|
queryStats.lastSlow = []
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
async function initDatabase() {
|
|
|
|
|
try {
|
|
|
|
|
const connection = await mysql.createConnection({
|
|
|
|
|
host: config.host,
|
|
|
|
|
port: config.port,
|
|
|
|
|
user: config.user,
|
2026-06-03 14:15:55 +08:00
|
|
|
password: config.password
|
2026-05-23 14:15:45 +08:00
|
|
|
})
|
2026-06-03 14:15:55 +08:00
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
await connection.query(`CREATE DATABASE IF NOT EXISTS ${config.database} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`)
|
|
|
|
|
await connection.query(`USE ${config.database}`)
|
2026-06-03 14:15:55 +08:00
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
const fs = require('fs')
|
|
|
|
|
const path = require('path')
|
|
|
|
|
const schema = fs.readFileSync(path.join(__dirname, 'schema.sql'), 'utf8')
|
|
|
|
|
const statements = schema.split(';').filter(s => s.trim())
|
2026-06-03 14:15:55 +08:00
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
for (const statement of statements) {
|
2026-06-03 14:15:55 +08:00
|
|
|
try {
|
|
|
|
|
await connection.query(statement)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (/Duplicate column name|already exists|exists/i.test(e.message)) continue
|
|
|
|
|
throw e
|
|
|
|
|
}
|
2026-05-23 14:15:45 +08:00
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
await connection.end()
|
|
|
|
|
console.log('Database initialized successfully')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to initialize database:', error)
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 13:37:55 +08:00
|
|
|
async function transaction(callback) {
|
|
|
|
|
const connection = await pool.getConnection()
|
|
|
|
|
try {
|
|
|
|
|
await connection.beginTransaction()
|
|
|
|
|
const result = await callback(connection)
|
|
|
|
|
await connection.commit()
|
|
|
|
|
return result
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await connection.rollback()
|
|
|
|
|
throw error
|
|
|
|
|
} finally {
|
|
|
|
|
connection.release()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
let healthCheckTimer = null
|
|
|
|
|
|
|
|
|
|
function startHealthCheck(intervalMs = 30000) {
|
|
|
|
|
if (healthCheckTimer) clearInterval(healthCheckTimer)
|
|
|
|
|
healthCheckTimer = setInterval(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection()
|
|
|
|
|
await connection.ping()
|
|
|
|
|
connection.release()
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Database health check failed:', err.message)
|
|
|
|
|
}
|
|
|
|
|
}, intervalMs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopHealthCheck() {
|
|
|
|
|
if (healthCheckTimer) {
|
|
|
|
|
clearInterval(healthCheckTimer)
|
|
|
|
|
healthCheckTimer = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
module.exports = {
|
|
|
|
|
pool,
|
|
|
|
|
query,
|
2026-05-26 13:37:55 +08:00
|
|
|
transaction,
|
2026-05-23 14:15:45 +08:00
|
|
|
initDatabase,
|
2026-06-03 14:15:55 +08:00
|
|
|
startHealthCheck,
|
|
|
|
|
stopHealthCheck,
|
|
|
|
|
getPoolMetrics,
|
|
|
|
|
getQueryStats,
|
|
|
|
|
resetQueryStats,
|
2026-05-23 14:15:45 +08:00
|
|
|
config
|
|
|
|
|
}
|