import { ConfigStore } from './ConfigStore' import { exec, spawn } from 'child_process' import { promisify } from 'util' import { existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync, rmdirSync, mkdirSync, renameSync } from 'fs' import { join, dirname } from 'path' import https from 'https' import http from 'http' import { createWriteStream } from 'fs' import { sendDownloadProgress } from '../main' const execAsync = promisify(exec) interface MysqlVersion { version: string path: string isRunning: boolean } interface AvailableMysqlVersion { version: string downloadUrl: string mirrors?: string[] // 备用下载链接 } // 阿里云镜像基础地址 const ALIYUN_MIRROR_BASE = 'https://mirrors.aliyun.com/mysql/' // 搜狐镜像作为备用 const SOHU_MIRROR_BASE = 'https://mirrors.sohu.com/mysql/' export class MysqlManager { private configStore: ConfigStore private mysqlProcess: any = null private cachedVersions: AvailableMysqlVersion[] | null = null private cacheTime: number = 0 private readonly CACHE_DURATION = 5 * 60 * 1000 // 缓存5分钟 constructor(configStore: ConfigStore) { this.configStore = configStore } /** * 获取已安装的 MySQL 版本列表 */ async getInstalledVersions(): Promise { const versions: MysqlVersion[] = [] const mysqlDir = join(this.configStore.getBasePath(), 'mysql') if (!existsSync(mysqlDir)) { return versions } const dirs = readdirSync(mysqlDir, { withFileTypes: true }) for (const dir of dirs) { if (dir.isDirectory() && dir.name.startsWith('mysql-')) { const version = dir.name.replace('mysql-', '') const mysqlPath = join(mysqlDir, dir.name) // 验证 MySQL 是否真的存在 if (existsSync(join(mysqlPath, 'bin', 'mysqld.exe'))) { const isRunning = await this.checkIsRunning(version) versions.push({ version, path: mysqlPath, isRunning }) } } } return versions.sort((a, b) => b.version.localeCompare(a.version)) } /** * 获取可用的 MySQL 版本列表(从阿里云镜像动态获取) * 镜像地址: https://mirrors.aliyun.com/mysql/ */ async getAvailableVersions(): Promise { // 检查缓存 if (this.cachedVersions && Date.now() - this.cacheTime < this.CACHE_DURATION) { console.log('使用缓存的 MySQL 版本列表') const installed = await this.getInstalledVersions() const installedVersions = installed.map(v => v.version) return this.cachedVersions.filter(v => !installedVersions.includes(v.version)) } let versions: AvailableMysqlVersion[] = [] try { console.log('从阿里云镜像获取 MySQL 版本列表...') // 1. 获取主目录页面,解析出 MySQL-X.X 目录 const mainPageHtml = await this.fetchHtml(ALIYUN_MIRROR_BASE) // 匹配 MySQL-5.7, MySQL-8.0 等目录(只获取5.7和8.x系列) const dirRegex = //g const mysqlDirs: string[] = [] let match while ((match = dirRegex.exec(mainPageHtml)) !== null) { mysqlDirs.push(match[1]) } console.log(`找到 ${mysqlDirs.length} 个 MySQL 版本目录: ${mysqlDirs.join(', ')}`) // 2. 遍历每个目录,获取 Windows 版本的 zip 文件 for (const dir of mysqlDirs) { try { const dirUrl = `${ALIYUN_MIRROR_BASE}${dir}` const dirHtml = await this.fetchHtml(dirUrl) // 匹配 mysql-X.X.XX-winx64.zip 格式的文件 const fileRegex = //g while ((match = fileRegex.exec(dirHtml)) !== null) { const fileName = match[1] const version = match[2] const downloadUrl = `${ALIYUN_MIRROR_BASE}${dir}${fileName}` // 添加搜狐镜像作为备用 const majorMinor = version.split('.').slice(0, 2).join('.') const sohuUrl = `${SOHU_MIRROR_BASE}MySQL-${majorMinor}/${fileName}` versions.push({ version, downloadUrl, mirrors: [sohuUrl] }) } } catch (dirError) { console.error(`获取目录 ${dir} 失败:`, dirError) } } // 按版本号降序排序 versions.sort((a, b) => { const aParts = a.version.split('.').map(Number) const bParts = b.version.split('.').map(Number) for (let i = 0; i < 3; i++) { if (bParts[i] !== aParts[i]) { return bParts[i] - aParts[i] } } return 0 }) // 去重(保留最新版本) const seen = new Set() versions = versions.filter(v => { if (seen.has(v.version)) return false seen.add(v.version) return true }) console.log(`从阿里云镜像获取到 ${versions.length} 个 MySQL 版本`) // 更新缓存 this.cachedVersions = versions this.cacheTime = Date.now() } catch (error) { console.error('从阿里云镜像获取版本列表失败,使用备用列表:', error) // 备用列表 versions = [ { version: '8.0.28', downloadUrl: 'https://mirrors.sohu.com/mysql/MySQL-8.0/mysql-8.0.28-winx64.zip', mirrors: [] }, { version: '8.0.27', downloadUrl: 'https://mirrors.sohu.com/mysql/MySQL-8.0/mysql-8.0.27-winx64.zip', mirrors: [] }, { version: '5.7.38', downloadUrl: 'https://mirrors.sohu.com/mysql/MySQL-5.7/mysql-5.7.38-winx64.zip', mirrors: [] }, { version: '5.7.37', downloadUrl: 'https://mirrors.sohu.com/mysql/MySQL-5.7/mysql-5.7.37-winx64.zip', mirrors: [] } ] } // 过滤掉已安装的版本 const installed = await this.getInstalledVersions() const installedVersions = installed.map(v => v.version) return versions.filter(v => !installedVersions.includes(v.version)) } /** * 获取网页 HTML 内容 */ private async fetchHtml(url: string): Promise { return new Promise((resolve, reject) => { const protocol = url.startsWith('https') ? https : http const request = protocol.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }, (response) => { // 处理重定向 if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) { const redirectUrl = response.headers.location if (redirectUrl) { this.fetchHtml(redirectUrl).then(resolve).catch(reject) return } } if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}`)) return } let html = '' response.on('data', (chunk) => (html += chunk)) response.on('end', () => resolve(html)) response.on('error', reject) }) request.on('error', reject) request.setTimeout(15000, () => { request.destroy() reject(new Error('请求超时')) }) }) } /** * 安装 MySQL 版本 */ async install(version: string): Promise<{ success: boolean; message: string }> { try { const available = await this.getAvailableVersions() const versionInfo = available.find(v => v.version === version) if (!versionInfo) { return { success: false, message: `未找到 MySQL ${version} 版本` } } const mysqlPath = this.configStore.getMysqlPath(version) const tempPath = this.configStore.getTempPath() const mysqlBaseDir = join(this.configStore.getBasePath(), 'mysql') const zipPath = join(tempPath, `mysql-${version}.zip`) const dataPath = join(mysqlPath, 'data') // 确保目录存在 if (!existsSync(tempPath)) { mkdirSync(tempPath, { recursive: true }) } if (!existsSync(mysqlBaseDir)) { mkdirSync(mysqlBaseDir, { recursive: true }) } // 尝试下载 MySQL(自动尝试备用链接) const allUrls = [versionInfo.downloadUrl, ...(versionInfo.mirrors || [])] let downloadSuccess = false let lastError = '' for (const url of allUrls) { try { console.log(`尝试下载 MySQL ${version} 从 ${url}`) await this.downloadFile(url, zipPath) downloadSuccess = true console.log('下载完成!') break } catch (error: any) { lastError = error.message console.log(`下载失败: ${error.message},尝试下一个镜像...`) // 清理失败的下载 if (existsSync(zipPath)) { try { unlinkSync(zipPath) } catch (e) {} } } } if (!downloadSuccess) { return { success: false, message: `所有下载源均失败: ${lastError}` } } console.log('开始解压...') // 解压到 mysql 目录 await this.unzip(zipPath, mysqlBaseDir) console.log('解压完成,处理目录...') // 重命名目录(MySQL 解压后目录名带完整版本号) const extractedDir = join(mysqlBaseDir, `mysql-${version}-winx64`) if (existsSync(extractedDir) && !existsSync(mysqlPath)) { try { renameSync(extractedDir, mysqlPath) console.log(`目录重命名: ${extractedDir} -> ${mysqlPath}`) } catch (renameError: any) { console.error('重命名失败,尝试复制:', renameError.message) // 如果重命名失败,可能是跨盘符,尝试使用 fs-extra 或手动复制 } } // 删除临时文件 if (existsSync(zipPath)) { try { unlinkSync(zipPath) } catch (e) { console.log('清理临时文件失败,忽略') } } // 创建 data 目录 if (!existsSync(dataPath)) { mkdirSync(dataPath, { recursive: true }) } // 创建 my.ini 配置文件 await this.createDefaultConfig(mysqlPath, version) // 初始化 MySQL console.log('初始化 MySQL 数据库...') await this.initializeDatabase(mysqlPath) // 添加到配置 this.configStore.addMysqlVersion(version) // 启动 MySQL 并设置默认密码 console.log('启动 MySQL 并设置默认密码...') const startResult = await this.start(version) if (startResult.success) { // 等待 MySQL 完全就绪 await new Promise(resolve => setTimeout(resolve, 3000)) // 设置默认密码 123456 try { await this.setInitialPassword(mysqlPath, '123456') console.log('默认密码设置成功: 123456') } catch (pwdError: any) { console.log('设置默认密码失败,root密码为空:', pwdError.message) } } console.log(`MySQL ${version} 安装完成`) return { success: true, message: `MySQL ${version} 安装成功!\n\n连接信息:\n• 主机:localhost\n• 端口:3306\n• 用户:root\n• 密码:123456` } } catch (error: any) { console.error('MySQL 安装失败:', error) return { success: false, message: `安装失败: ${error.message}` } } } /** * 卸载 MySQL 版本 */ async uninstall(version: string): Promise<{ success: boolean; message: string }> { try { // 先停止服务 await this.stop(version) const mysqlPath = this.configStore.getMysqlPath(version) if (!existsSync(mysqlPath)) { return { success: false, message: `MySQL ${version} 未安装` } } // 移除 Windows 服务 try { await execAsync(`"${join(mysqlPath, 'bin', 'mysqld.exe')}" --remove MySQL${version.replace(/\./g, '')}`) } catch (e) { // 忽略服务移除错误 } // 递归删除目录 this.removeDirectory(mysqlPath) // 从配置中移除 this.configStore.removeMysqlVersion(version) return { success: true, message: `MySQL ${version} 已卸载` } } catch (error: any) { return { success: false, message: `卸载失败: ${error.message}` } } } /** * 启动 MySQL 服务 */ async start(version: string): Promise<{ success: boolean; message: string }> { try { const mysqlPath = this.configStore.getMysqlPath(version) const mysqldPath = join(mysqlPath, 'bin', 'mysqld.exe') const configPath = join(mysqlPath, 'my.ini') const logsPath = this.configStore.getLogsPath() const tempPath = this.configStore.getTempPath() if (!existsSync(mysqldPath)) { return { success: false, message: `MySQL ${version} 未安装` } } // 确保临时目录存在 if (!existsSync(tempPath)) { mkdirSync(tempPath, { recursive: true }) } // 检查是否已在运行 const isRunning = await this.checkIsRunning(version) if (isRunning) { return { success: true, message: `MySQL ${version} 已经在运行` } } // 检查并修复配置文件(MySQL 8.0 兼容性) await this.fixConfigIfNeeded(mysqlPath, version) // 检查数据目录是否正确初始化 const dataPath = join(mysqlPath, 'data') const needsInit = await this.checkNeedsInitialize(dataPath) if (needsInit) { console.log('检测到数据库未正确初始化,正在重新初始化...') // 清空数据目录 if (existsSync(dataPath)) { this.removeDirectory(dataPath) } mkdirSync(dataPath, { recursive: true }) // 重新初始化 await this.initializeDatabase(mysqlPath) } console.log(`启动 MySQL ${version}...`) console.log(`mysqld 路径: ${mysqldPath}`) console.log(`配置文件: ${configPath}`) // 创建 VBScript 来隐藏窗口启动 MySQL const vbsPath = join(tempPath, 'start_mysql.vbs') const vbsContent = ` Set WshShell = CreateObject("WScript.Shell") WshShell.Run """${mysqldPath.replace(/\\/g, '\\\\')}""" & " --defaults-file=""${configPath.replace(/\\/g, '\\\\')}"" ", 0, False ` writeFileSync(vbsPath, vbsContent.trim()) // 使用 wscript 执行 VBS,完全无窗口 const child = spawn('wscript.exe', [vbsPath], { detached: true, stdio: 'ignore', windowsHide: true }) child.unref() // 删除临时 VBS 文件(延迟删除) setTimeout(() => { try { if (existsSync(vbsPath)) unlinkSync(vbsPath) } catch (e) {} }, 5000) // 等待启动(多次检测,增加等待时间给 PowerShell) await new Promise(resolve => setTimeout(resolve, 2000)) // 先等待 PowerShell 启动进程 let running = false for (let i = 0; i < 15; i++) { await new Promise(resolve => setTimeout(resolve, 1000)) running = await this.checkIsRunning(version) if (running) { console.log(`MySQL ${version} 启动成功 (${i + 1}秒)`) break } console.log(`等待 MySQL 启动... (${i + 1}/15)`) } if (running) { return { success: true, message: `MySQL ${version} 启动成功,端口 3306` } } else { // 再检查一次进程是否存在 try { const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq mysqld.exe" /NH', { windowsHide: true }) if (stdout.includes('mysqld.exe')) { console.log('检测到 mysqld 进程存在,可能端口检测有问题') return { success: true, message: `MySQL ${version} 已启动(进程运行中)` } } } catch (e) {} // 读取错误日志 const errorLogPath = join(logsPath, 'mysql-error.log') let errorInfo = '' if (existsSync(errorLogPath)) { try { const logContent = readFileSync(errorLogPath, 'utf-8') const lines = logContent.split('\n').slice(-10) // 最后10行 errorInfo = lines.join('\n') } catch (e) {} } return { success: false, message: `MySQL ${version} 启动失败。${errorInfo ? '\n错误日志:\n' + errorInfo : '请检查日志文件'}` } } } catch (error: any) { console.error(`启动 MySQL 失败:`, error) return { success: false, message: `启动失败: ${error.message}` } } } /** * 停止 MySQL 服务 */ async stop(version: string): Promise<{ success: boolean; message: string }> { try { const mysqlPath = this.configStore.getMysqlPath(version) const mysqladminPath = join(mysqlPath, 'bin', 'mysqladmin.exe') console.log(`停止 MySQL ${version}...`) // 尝试优雅关闭 try { await execAsync(`"${mysqladminPath}" -u root shutdown`, { timeout: 10000, windowsHide: true }) } catch (e) { console.log('mysqladmin shutdown 失败,尝试 taskkill') // 如果优雅关闭失败,尝试强制结束进程 try { await execAsync('taskkill /F /IM mysqld.exe', { timeout: 5000, windowsHide: true }) } catch (e2) { // 进程可能不存在 console.log('taskkill 失败,进程可能不存在') } } // 等待进程结束 await new Promise(resolve => setTimeout(resolve, 2000)) const isRunning = await this.checkIsRunning(version) if (!isRunning) { console.log(`MySQL ${version} 已停止`) return { success: true, message: `MySQL ${version} 已停止` } } else { return { success: false, message: `MySQL ${version} 停止失败` } } } catch (error: any) { return { success: false, message: `停止失败: ${error.message}` } } } /** * 重启 MySQL 服务 */ async restart(version: string): Promise<{ success: boolean; message: string }> { const stopResult = await this.stop(version) if (!stopResult.success && !stopResult.message.includes('已停止')) { return stopResult } await new Promise(resolve => setTimeout(resolve, 1000)) return await this.start(version) } /** * 获取 MySQL 运行状态 */ async getStatus(version: string): Promise<{ running: boolean; pid?: number; uptime?: string }> { const isRunning = await this.checkIsRunning(version) if (!isRunning) { return { running: false } } try { const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq mysqld.exe" /FO CSV /NH') const lines = stdout.trim().split('\n') if (lines.length > 0 && lines[0].includes('mysqld.exe')) { const parts = lines[0].split(',') const pid = parseInt(parts[1].replace(/"/g, '')) return { running: true, pid } } } catch (e) { // 忽略错误 } return { running: isRunning } } /** * 修改 root 密码 */ async changeRootPassword(version: string, newPassword: string, currentPassword?: string): Promise<{ success: boolean; message: string }> { try { const mysqlPath = this.configStore.getMysqlPath(version) const mysqlExe = join(mysqlPath, 'bin', 'mysql.exe') // 检查 MySQL 是否在运行 const isRunning = await this.checkIsRunning(version) if (!isRunning) { return { success: false, message: 'MySQL 未运行,请先启动服务' } } const escapedNewPwd = newPassword.replace(/'/g, "\\'") // 修改所有 root 用户的密码 const sql = ` ALTER USER 'root'@'localhost' IDENTIFIED BY '${escapedNewPwd}'; ALTER USER IF EXISTS 'root'@'127.0.0.1' IDENTIFIED BY '${escapedNewPwd}'; ALTER USER IF EXISTS 'root'@'%' IDENTIFIED BY '${escapedNewPwd}'; FLUSH PRIVILEGES; `.replace(/\n/g, ' ').trim() // 构建命令,如果提供了当前密码则使用 let cmd = `"${mysqlExe}" -u root --host=localhost` if (currentPassword) { cmd += ` -p"${currentPassword.replace(/"/g, '\\"')}"` } cmd += ` -e "${sql}"` await execAsync(cmd, { timeout: 15000, windowsHide: true }) return { success: true, message: 'root 密码修改成功' } } catch (error: any) { // 检查是否是密码错误 if (error.message.includes('Access denied')) { return { success: false, message: '当前密码错误,请输入正确的当前密码' } } return { success: false, message: `密码修改失败: ${error.message}` } } } /** * 获取 my.ini 配置内容 */ async getConfig(version: string): Promise { const mysqlPath = this.configStore.getMysqlPath(version) const configPath = join(mysqlPath, 'my.ini') if (!existsSync(configPath)) { return '' } return readFileSync(configPath, 'utf-8') } /** * 保存 my.ini 配置 */ async saveConfig(version: string, config: string): Promise<{ success: boolean; message: string }> { try { const mysqlPath = this.configStore.getMysqlPath(version) const configPath = join(mysqlPath, 'my.ini') writeFileSync(configPath, config) return { success: true, message: 'my.ini 保存成功,需要重启 MySQL 生效' } } catch (error: any) { return { success: false, message: `保存失败: ${error.message}` } } } /** * 设置初始密码(首次安装后使用) */ private async setInitialPassword(mysqlPath: string, password: string): Promise { const mysqlExe = join(mysqlPath, 'bin', 'mysql.exe') // 使用空密码连接并设置新密码,同时授权 127.0.0.1 访问 const sql = ` ALTER USER 'root'@'localhost' IDENTIFIED BY '${password}'; CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY '${password}'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION; CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY '${password}'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; `.replace(/\n/g, ' ').trim() try { // 使用 --host=localhost 确保通过 socket 连接 await execAsync( `"${mysqlExe}" -u root --host=localhost -e "${sql}"`, { timeout: 15000, windowsHide: true, cwd: join(mysqlPath, 'bin') } ) } catch (error: any) { throw new Error(`设置密码失败: ${error.message}`) } } /** * 重新初始化数据库(会清空所有数据!) */ async reinitialize(version: string): Promise<{ success: boolean; message: string }> { try { // 先停止服务 await this.stop(version) const mysqlPath = this.configStore.getMysqlPath(version) const dataPath = join(mysqlPath, 'data') console.log(`重新初始化 MySQL ${version}...`) // 删除数据目录 if (existsSync(dataPath)) { console.log('删除旧数据目录...') this.removeDirectory(dataPath) } // 创建新的数据目录 mkdirSync(dataPath, { recursive: true }) // 重新生成配置文件 await this.createDefaultConfig(mysqlPath, version) // 初始化数据库 await this.initializeDatabase(mysqlPath) // 启动并设置默认密码 console.log('启动 MySQL 并设置默认密码...') const startResult = await this.start(version) if (startResult.success) { await new Promise(resolve => setTimeout(resolve, 3000)) try { await this.setInitialPassword(mysqlPath, '123456') console.log('默认密码设置成功: 123456') } catch (e) { console.log('设置默认密码失败') } } console.log(`MySQL ${version} 重新初始化完成`) return { success: true, message: `MySQL ${version} 重新初始化成功!\n\n连接信息:\n• 用户:root\n• 密码:123456` } } catch (error: any) { console.error('重新初始化失败:', error) return { success: false, message: `重新初始化失败: ${error.message}` } } } // ==================== 私有方法 ==================== /** * 检查数据库是否需要初始化 */ private async checkNeedsInitialize(dataPath: string): Promise { if (!existsSync(dataPath)) { return true } const files = readdirSync(dataPath) if (files.length === 0) { return true } // 检查关键的 mysql 系统数据库目录是否存在 const mysqlDbPath = join(dataPath, 'mysql') if (!existsSync(mysqlDbPath)) { console.log('mysql 系统数据库目录不存在,需要重新初始化') return true } // 检查是否有必要的系统文件 const requiredFiles = ['ibdata1', 'ib_logfile0'] for (const file of requiredFiles) { // MySQL 8.0.30+ 可能没有 ib_logfile,所以只检查 ibdata1 if (file === 'ibdata1' && !existsSync(join(dataPath, file))) { console.log(`缺少 ${file},需要重新初始化`) return true } } return false } /** * 检查并修复配置文件(MySQL 8.0 兼容性) */ private async fixConfigIfNeeded(mysqlPath: string, version: string): Promise { const configPath = join(mysqlPath, 'my.ini') if (!existsSync(configPath)) { console.log('配置文件不存在,重新创建') await this.createDefaultConfig(mysqlPath, version) return } const majorVersion = parseInt(version.split('.')[0]) const isMySQL8 = majorVersion >= 8 if (isMySQL8) { let content = readFileSync(configPath, 'utf-8') let needsUpdate = false // 检查是否包含 MySQL 8.0 不支持的配置 const deprecatedConfigs = [ /^query_cache_type\s*=.*$/gm, /^query_cache_size\s*=.*$/gm, /^query_cache_limit\s*=.*$/gm, /^innodb_log_file_size\s*=.*$/gm, // 8.0.30+ 自动管理 ] for (const regex of deprecatedConfigs) { if (regex.test(content)) { content = content.replace(regex, '# [已移除 - MySQL 8.0 不支持]') needsUpdate = true } } if (needsUpdate) { console.log('检测到 MySQL 8.0 不兼容配置,正在修复...') writeFileSync(configPath, content) console.log('配置文件已修复') } } } private async checkIsRunning(version: string): Promise { try { const mysqlPath = this.configStore.getMysqlPath(version) const port = this.getPortFromConfig(mysqlPath) const { stdout } = await execAsync(`netstat -ano | findstr ":${port}"`) return stdout.includes('LISTENING') } catch (e) { return false } } private getPortFromConfig(mysqlPath: string): number { const configPath = join(mysqlPath, 'my.ini') if (existsSync(configPath)) { const content = readFileSync(configPath, 'utf-8') const match = content.match(/port\s*=\s*(\d+)/) if (match) { return parseInt(match[1]) } } return 3306 } private async downloadFile(url: string, dest: string): Promise { // 确保目标目录存在 const destDir = dirname(dest) if (!existsSync(destDir)) { mkdirSync(destDir, { recursive: true }) } return new Promise((resolve, reject) => { const file = createWriteStream(dest) const protocol = url.startsWith('https') ? https : http console.log(`下载: ${url}`) const request = protocol.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }, (response) => { // 处理重定向 if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) { const redirectUrl = response.headers.location if (redirectUrl) { file.close() if (existsSync(dest)) unlinkSync(dest) console.log(`重定向到: ${redirectUrl}`) this.downloadFile(redirectUrl, dest).then(resolve).catch(reject) return } } if (response.statusCode !== 200) { file.close() if (existsSync(dest)) unlinkSync(dest) reject(new Error(`下载失败,状态码: ${response.statusCode}, URL: ${url}`)) return } const totalSize = parseInt(response.headers['content-length'] || '0', 10) let downloadedSize = 0 let lastProgressTime = Date.now() response.on('data', (chunk) => { downloadedSize += chunk.length const now = Date.now() // 每500ms发送一次进度 if (now - lastProgressTime > 500) { const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0 sendDownloadProgress('mysql', progress, downloadedSize, totalSize) lastProgressTime = now } }) response.pipe(file) file.on('finish', () => { file.close() sendDownloadProgress('mysql', 100, totalSize, totalSize) console.log('下载完成') resolve() }) file.on('error', (err) => { file.close() if (existsSync(dest)) unlinkSync(dest) reject(err) }) }) request.on('error', (err) => { file.close() if (existsSync(dest)) unlinkSync(dest) reject(new Error(`网络错误: ${err.message}`)) }) // 10 分钟超时(MySQL 包较大) request.setTimeout(600000, () => { request.destroy() file.close() if (existsSync(dest)) unlinkSync(dest) reject(new Error('下载超时(10分钟)')) }) }) } private async unzip(zipPath: string, destPath: string): Promise { // 确保目标目录存在 if (!existsSync(destPath)) { mkdirSync(destPath, { recursive: true }) } const { createReadStream } = await import('fs') const unzipper = await import('unzipper') return new Promise((resolve, reject) => { console.log(`解压: ${zipPath} -> ${destPath}`) const stream = createReadStream(zipPath) .pipe(unzipper.Extract({ path: destPath })) stream.on('close', () => { console.log('解压完成') resolve() }) stream.on('error', (err: Error) => { console.error('解压错误:', err) reject(new Error(`解压失败: ${err.message}`)) }) }) } private async createDefaultConfig(mysqlPath: string, version: string): Promise { const configPath = join(mysqlPath, 'my.ini') const dataPath = join(mysqlPath, 'data') const logsPath = this.configStore.getLogsPath() // 确保日志目录存在 if (!existsSync(logsPath)) { mkdirSync(logsPath, { recursive: true }) } // 判断是否是 MySQL 8.0+ const majorVersion = parseInt(version.split('.')[0]) const isMySQL8 = majorVersion >= 8 // MySQL 8.0 移除了 query_cache,需要区分配置 const cacheConfig = isMySQL8 ? '' : ` # 查询缓存 (仅 MySQL 5.7) query_cache_type=1 query_cache_size=64M` const config = `[mysqld] # 基础配置 basedir=${mysqlPath.replace(/\\/g, '/')} datadir=${dataPath.replace(/\\/g, '/')} port=3306 bind-address=0.0.0.0 server-id=1 # 字符集 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci # 连接数 max_connections=200 max_connect_errors=10 # 存储引擎 default-storage-engine=INNODB # 日志 log-error=${logsPath.replace(/\\/g, '/')}/mysql-error.log slow_query_log=1 slow_query_log_file=${logsPath.replace(/\\/g, '/')}/mysql-slow.log long_query_time=10 ${cacheConfig} # 表缓存 table_open_cache=2000 tmp_table_size=64M max_heap_table_size=64M # InnoDB innodb_buffer_pool_size=256M innodb_log_buffer_size=16M innodb_flush_log_at_trx_commit=1 innodb_lock_wait_timeout=50 # 跳过时区设置(避免初始化问题) # default-time-zone='+8:00' # 允许本地加载数据 local_infile=1 # 注意:不要启用 skip-name-resolve,否则 localhost 和 127.0.0.1 会被视为不同主机 [mysql] default-character-set=utf8mb4 [client] port=3306 default-character-set=utf8mb4 ` writeFileSync(configPath, config) console.log(`创建 MySQL ${version} 配置文件: ${configPath}`) } private async initializeDatabase(mysqlPath: string): Promise { const mysqldPath = join(mysqlPath, 'bin', 'mysqld.exe') const configPath = join(mysqlPath, 'my.ini') const dataPath = join(mysqlPath, 'data') // 检查是否已经初始化过 if (existsSync(dataPath)) { const files = readdirSync(dataPath) if (files.length > 0) { console.log('MySQL 数据目录不为空,跳过初始化') return } } console.log('开始初始化 MySQL 数据库...') // 使用 execAsync 初始化数据库(初始化是一次性操作,可以等待完成) return new Promise(async (resolve, reject) => { try { // 初始化命令需要等待完成 await execAsync( `"${mysqldPath}" --defaults-file="${configPath}" --initialize-insecure`, { timeout: 120000, windowsHide: true, cwd: join(mysqlPath, 'bin') } ) console.log('MySQL 初始化命令执行完成') } catch (error: any) { console.log('MySQL 初始化命令返回:', error.message) } // 检查数据目录是否有文件 setTimeout(() => { const files = existsSync(dataPath) ? readdirSync(dataPath) : [] if (files.length > 0) { console.log('数据目录已创建,初始化成功') resolve() } else { reject(new Error('初始化失败,数据目录为空')) } }, 2000) }) } private removeDirectory(dir: string): void { if (existsSync(dir)) { const files = readdirSync(dir, { withFileTypes: true }) for (const file of files) { const fullPath = join(dir, file.name) if (file.isDirectory()) { this.removeDirectory(fullPath) } else { unlinkSync(fullPath) } } rmdirSync(dir) } } }