C端打包修复不能在中文路径下启动的问题

This commit is contained in:
2026-04-02 20:51:19 +08:00
parent ad02fac4ff
commit 926b85bf8d
19 changed files with 474 additions and 373 deletions

View File

@@ -1,5 +1,6 @@
const fs = require('fs');
const path = require('path');
const { resolveRuntimeStrategy } = require('./path-utils');
/**
* 配置文件生成器
@@ -8,11 +9,12 @@ const path = require('path');
class ConfigGenerator {
constructor() {
// 开发环境:项目根目录
// 打包后:应用根目录(win-unpacked
// 打包后:应用根目录(最终交付目录
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')
: path.dirname(process.resourcesPath);
const pathStrategy = resolveRuntimeStrategy(baseDir);
// 开发环境build/extraResources/java
// 打包后resources/extraResources/java
@@ -21,9 +23,12 @@ class ConfigGenerator {
: path.join(process.resourcesPath, 'extraResources', 'java');
this.templatePath = path.join(this.javaPath, 'application.yml.template');
this.configPath = path.join(this.javaPath, 'application.yml');
this.pathStrategy = pathStrategy;
// 数据目录使用应用所在盘符的根目录下的data文件夹
this.dataPath = this.getDataPath(baseDir);
// 数据目录
// - 安全路径:使用应用目录内的 NPQS9100_Data
// - 非 ASCII 路径:切到英文安全运行根目录下,避免在盘符根目录创建多个文件夹
this.dataPath = this.getDataPath(baseDir, pathStrategy);
}
/**
@@ -31,8 +36,11 @@ class ConfigGenerator {
* @param {string} baseDir 应用基础目录
* @returns {string} 数据目录路径
*/
getDataPath(baseDir) {
// 数据目录设置在应用目录内的 NPQS9100_Data 文件夹
getDataPath(baseDir, pathStrategy = resolveRuntimeStrategy(baseDir)) {
if (pathStrategy.usesSafePaths) {
return path.join(pathStrategy.safeRuntimeRoot, 'data');
}
return path.join(baseDir, 'NPQS9100_Data');
}
@@ -85,6 +93,7 @@ class ConfigGenerator {
this.createDirectories();
console.log('[ConfigGenerator] Configuration file generated successfully');
console.log('[ConfigGenerator] Path mode:', this.pathStrategy.usesSafePaths ? 'safe-data-root' : 'app-local-data');
console.log('[ConfigGenerator] Data path:', this.dataPath);
console.log('[ConfigGenerator] MySQL port:', options.mysqlPort || 3306);
console.log('[ConfigGenerator] MySQL password:', options.mysqlPassword || 'njcnpqs');

View File

@@ -9,7 +9,7 @@ class JavaRunner {
constructor() {
// 在开发与打包后均可解析到应用根目录下的 jre 目录
// 开发环境:项目根目录
// 打包后:应用根目录(win-unpacked
// 打包后:应用根目录(最终交付目录
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')

View File

@@ -1,19 +1,26 @@
const { spawn, exec } = require('child_process');
const path = require('path');
const fs = require('fs');
const { resolveRuntimeStrategy } = require('./path-utils');
/**
* MySQL 进程管理器
* 使用进程模式启动 MySQL无需管理员权限
*/
class MySQLServiceManager {
class MySQLProcessManager {
constructor(logWindowManager = null) {
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')
: path.dirname(process.resourcesPath);
const pathStrategy = resolveRuntimeStrategy(baseDir);
this.mysqlPath = path.join(baseDir, 'mysql');
this.baseDir = baseDir;
this.pathStrategy = pathStrategy;
this.sourceMysqlPath = path.join(baseDir, 'mysql');
this.mysqlPath = pathStrategy.usesSafePaths
? path.join(pathStrategy.safeRuntimeRoot, 'mysql')
: this.sourceMysqlPath;
this.binPath = path.join(this.mysqlPath, 'bin');
this.dataPath = path.join(this.mysqlPath, 'data');
this.configFile = path.join(this.mysqlPath, 'my.ini');
@@ -37,6 +44,106 @@ class MySQLServiceManager {
}
}
/**
* 递归复制目录
* 仅当文件不存在大小变化或源文件时间更新时才覆盖避免每次启动全量重写
*/
copyDirectorySync(sourceDir, targetDir, options = {}) {
const {
excludeTopLevelDirs = new Set(),
excludeFileNames = new Set(),
excludeFileExtensions = new Set()
} = options;
if (!fs.existsSync(sourceDir)) {
return;
}
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
entries.forEach((entry) => {
const sourcePath = path.join(sourceDir, entry.name);
const targetPath = path.join(targetDir, entry.name);
const isTopLevelEntry = sourceDir === this.sourceMysqlPath;
if (entry.isDirectory()) {
if (isTopLevelEntry && excludeTopLevelDirs.has(entry.name)) {
return;
}
this.copyDirectorySync(sourcePath, targetPath, options);
return;
}
const extension = path.extname(entry.name).toLowerCase();
if (excludeFileNames.has(entry.name) || excludeFileExtensions.has(extension)) {
return;
}
let shouldCopy = !fs.existsSync(targetPath);
if (!shouldCopy) {
const sourceStat = fs.statSync(sourcePath);
const targetStat = fs.statSync(targetPath);
shouldCopy = sourceStat.size !== targetStat.size || sourceStat.mtimeMs > targetStat.mtimeMs;
}
if (shouldCopy) {
fs.copyFileSync(sourcePath, targetPath);
}
});
}
/**
* ASCII 路径下 MySQL 运行环境同步到英文安全目录
*/
ensureRuntimeEnvironment() {
if (!this.pathStrategy.usesSafePaths) {
return;
}
this.log('system', '检测到应用路径包含非 ASCII 字符,启用 MySQL 英文安全运行目录');
this.log('system', `MySQL 源目录: ${this.sourceMysqlPath}`);
this.log('system', `MySQL 运行目录: ${this.mysqlPath}`);
this.log('system', `MySQL 数据目录: ${this.dataPath}`);
fs.mkdirSync(this.mysqlPath, { recursive: true });
fs.mkdirSync(path.dirname(this.dataPath), { recursive: true });
this.copyDirectorySync(this.sourceMysqlPath, this.mysqlPath, {
excludeTopLevelDirs: new Set(['data', 'data_backup']),
excludeFileNames: new Set(['my.ini', 'mysql_error.log', 'mysql_slow.log']),
excludeFileExtensions: new Set(['.pid', '.err'])
});
this.ensureSeedData();
}
/**
* 首次进入英文安全路径模式时将打包内预置数据库复制到安全数据目录
*/
ensureSeedData() {
const hasTargetData = fs.existsSync(this.dataPath) && fs.readdirSync(this.dataPath).length > 0;
if (hasTargetData) {
this.log('system', '检测到已有 MySQL 安全数据目录,直接复用');
return;
}
const sourceDataPath = path.join(this.sourceMysqlPath, 'data');
if (fs.existsSync(sourceDataPath) && fs.readdirSync(sourceDataPath).length > 0) {
this.log('system', '首次启动,正在复制预置 MySQL 数据到安全数据目录...');
this.copyDirectorySync(sourceDataPath, this.dataPath);
this.log('success', '预置 MySQL 数据复制完成');
return;
}
fs.mkdirSync(this.dataPath, { recursive: true });
this.log('warn', '未找到预置 MySQL 数据目录,后续将按空库初始化');
}
/**
* 执行命令并返回Promise
*/
@@ -352,7 +459,10 @@ default-character-set=utf8mb4
* @returns {Promise<number>} 返回 MySQL 运行的端口
*/
async ensureServiceRunning(findAvailablePort, waitForPort) {
this.ensureRuntimeEnvironment();
this.log('system', '开始 MySQL 进程检查流程(进程模式)');
this.log('system', `路径策略: ${this.pathStrategy.usesSafePaths ? '英文安全路径模式' : '应用目录直启模式'}`);
this.log('system', `MySQL 路径: ${this.mysqlPath}`);
this.log('system', `配置文件: ${this.configFile}`);
@@ -401,4 +511,4 @@ default-character-set=utf8mb4
}
}
module.exports = MySQLServiceManager;
module.exports = MySQLProcessManager;

41
scripts/path-utils.js Normal file
View File

@@ -0,0 +1,41 @@
const path = require('path');
/**
* 判断路径中是否包含非 ASCII 字符。
* 这里不只判断中文,其他非 ASCII 字符同样视为不安全路径。
*/
function hasNonAscii(targetPath = '') {
return /[^\x00-\x7F]/.test(targetPath);
}
/**
* 获取当前路径所在盘符根目录,例如 D:\
*/
function getDriveRoot(targetPath = '') {
const resolvedPath = path.resolve(targetPath || process.cwd());
return path.parse(resolvedPath).root;
}
/**
* 解析运行期路径策略。
* - 安全路径:继续直接使用应用目录
* - 非 ASCII 路径:切到英文安全路径
*/
function resolveRuntimeStrategy(baseDir) {
const normalizedBaseDir = path.resolve(baseDir);
const driveRoot = getDriveRoot(normalizedBaseDir);
const usesSafePaths = hasNonAscii(normalizedBaseDir);
return {
baseDir: normalizedBaseDir,
driveRoot,
usesSafePaths,
safeRuntimeRoot: path.join(driveRoot, 'NPQS9100_Runtime')
};
}
module.exports = {
hasNonAscii,
getDriveRoot,
resolveRuntimeStrategy
};