调整C端展示

This commit is contained in:
2025-11-26 08:50:22 +08:00
parent f987e1c625
commit 92a3076638
23 changed files with 715 additions and 1309 deletions

View File

@@ -32,10 +32,8 @@ class ConfigGenerator {
* @returns {string} 数据目录路径
*/
getDataPath(baseDir) {
// 获取应用所在盘符例如C:, D:, E:
const driveLetter = path.parse(baseDir).root;
// 数据目录设置在盘符根目录下的 NPQS9100_Data 文件夹
return path.join(driveLetter, 'NPQS9100_Data');
// 数据目录设置在应用目录内的 NPQS9100_Data 文件夹
return path.join(baseDir, 'NPQS9100_Data');
}
/**
@@ -43,6 +41,7 @@ class ConfigGenerator {
* @param {object} options - 配置选项
* @param {number} options.mysqlPort - MySQL 端口
* @param {number} options.javaPort - Java 应用端口
* @param {number} options.websocketPort - WebSocket 端口
* @param {string} options.mysqlPassword - MySQL 密码
*/
generateConfig(options = {}) {
@@ -73,6 +72,11 @@ class ConfigGenerator {
if (options.javaPort) {
template = template.replace(/port:\s*18092/g, `port: ${options.javaPort}`);
}
// 替换 WebSocket 端口
if (options.websocketPort) {
template = template.replace(/port:\s*7777/g, `port: ${options.websocketPort}`);
}
// 写入配置文件
fs.writeFileSync(this.configPath, template, 'utf-8');
@@ -85,12 +89,14 @@ class ConfigGenerator {
console.log('[ConfigGenerator] MySQL port:', options.mysqlPort || 3306);
console.log('[ConfigGenerator] MySQL password:', options.mysqlPassword || 'njcnpqs');
console.log('[ConfigGenerator] Java port:', options.javaPort || 18092);
console.log('[ConfigGenerator] WebSocket port:', options.websocketPort || 7777);
resolve({
configPath: this.configPath,
dataPath: this.dataPath,
mysqlPort: options.mysqlPort || 3306,
javaPort: options.javaPort || 18092
javaPort: options.javaPort || 18092,
websocketPort: options.websocketPort || 7777
});
} catch (error) {
console.error('[ConfigGenerator] Failed to generate config:', error);

View File

@@ -206,39 +206,109 @@ class JavaRunner {
* 停止 Spring Boot 应用
*/
stopSpringBoot() {
return new Promise((resolve) => {
return new Promise(async (resolve) => {
const { exec } = require('child_process');
const killedPids = new Set();
let killAttempts = 0;
// 方法1: 如果有进程引用通过PID杀死
if (this.springBootProcess && !this.springBootProcess.killed) {
// 设置3秒超时如果进程没有正常退出强制kill
const timeout = setTimeout(() => {
console.log('[Java] Force killing Spring Boot process');
try {
this.springBootProcess.kill('SIGKILL');
} catch (e) {
console.error('[Java] Error force killing:', e);
const pid = this.springBootProcess.pid;
console.log('[Java] Method 1: Stopping Spring Boot by PID:', pid);
// 使用 /F 强制终止,/T 终止子进程树
const killCommand = `taskkill /F /T /PID ${pid}`;
console.log('[Java] Executing:', killCommand);
exec(killCommand, (error, stdout, stderr) => {
if (error) {
console.error('[Java] taskkill by PID failed:', error);
} else {
console.log('[Java] taskkill by PID success:', stdout);
killedPids.add(pid);
}
// 清理端口记录文件
this.cleanupJavaPortFile();
resolve();
}, 3000);
this.springBootProcess.on('close', () => {
clearTimeout(timeout);
console.log('[Java] Spring Boot application stopped gracefully');
// 清理端口记录文件
this.cleanupJavaPortFile();
resolve();
killAttempts++;
checkComplete();
});
// 先尝试优雅关闭
console.log('[Java] Sending SIGTERM to Spring Boot');
this.springBootProcess.kill('SIGTERM');
} else {
// 即使没有进程引用,也尝试清理端口记录文件
this.cleanupJavaPortFile();
resolve();
killAttempts++;
}
// 方法2: 通过端口杀死占用进程精确定位不会误杀其他Java进程
const recordedPort = this.currentJavaPort || this.getRecordedJavaPort();
if (recordedPort) {
console.log(`[Java] Method 2: Killing process on port ${recordedPort} (precise targeting)`);
// 查找占用端口的进程
const findCommand = `netstat -ano | findstr :${recordedPort}`;
exec(findCommand, (error, stdout) => {
if (!error && stdout) {
// 提取PID最后一列
const lines = stdout.trim().split('\n');
const pids = new Set();
lines.forEach(line => {
const parts = line.trim().split(/\s+/);
const pid = parts[parts.length - 1];
if (pid && pid !== '0' && !killedPids.has(pid)) {
pids.add(pid);
}
});
console.log(`[Java] Found PIDs on port ${recordedPort}:`, Array.from(pids));
if (pids.size > 0) {
// 杀死所有找到的进程
let portsKilled = 0;
pids.forEach(pid => {
exec(`taskkill /F /T /PID ${pid}`, (err, out) => {
portsKilled++;
if (!err) {
console.log(`[Java] Killed process ${pid} on port ${recordedPort}`);
} else {
console.warn(`[Java] Failed to kill process ${pid}:`, err);
}
if (portsKilled === pids.size) {
killAttempts++;
checkComplete();
}
});
});
} else {
console.log(`[Java] No process found on port ${recordedPort} (already cleaned)`);
killAttempts++;
checkComplete();
}
} else {
console.log(`[Java] No process found on port ${recordedPort}`);
killAttempts++;
checkComplete();
}
});
} else {
console.log('[Java] No port recorded, skipping port-based kill');
killAttempts++;
}
// 检查是否所有清理方法都已完成
function checkComplete() {
const expectedAttempts = recordedPort ? 2 : 1;
if (killAttempts >= expectedAttempts) {
// 清理端口记录文件
this.cleanupJavaPortFile();
// 等待500ms确保进程完全终止
setTimeout(() => {
console.log('[Java] Spring Boot stop process completed');
console.log('[Java] Note: Other Java processes (like IDEA) are NOT affected');
resolve();
}, 500);
}
}
// 绑定this上下文
checkComplete = checkComplete.bind(this);
});
}

View File

@@ -11,17 +11,108 @@ class LogWindowManager {
this.logWindow = null;
this.logs = [];
this.maxLogs = 1000; // 最多保留1000条日志
// 初始化日志文件路径
this.initLogFile();
}
/**
* 初始化日志文件路径(按天滚动)
*/
initLogFile() {
// 开发环境:项目根目录的 logs 文件夹
// 打包后:应用根目录的 logs 文件夹
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')
: path.dirname(process.resourcesPath);
this.logsDir = path.join(baseDir, 'logs');
// 确保 logs 目录存在
if (!fs.existsSync(this.logsDir)) {
fs.mkdirSync(this.logsDir, { recursive: true });
}
// 生成当天的日志文件名startup-YYYYMMDD.log
const today = new Date();
const dateStr = today.getFullYear() +
String(today.getMonth() + 1).padStart(2, '0') +
String(today.getDate()).padStart(2, '0');
this.logFilePath = path.join(this.logsDir, `startup-${dateStr}.log`);
console.log('[LogWindowManager] Log file:', this.logFilePath);
// 写入启动标记
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', '=' .repeat(80));
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', 'NPQS9100 应用启动');
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', '=' .repeat(80));
// 清理超过30天的旧日志
this.cleanOldLogs(30);
}
/**
* 清理旧日志文件
* @param {number} days - 保留天数
*/
cleanOldLogs(days) {
try {
const now = Date.now();
const maxAge = days * 24 * 60 * 60 * 1000; // 转换为毫秒
const files = fs.readdirSync(this.logsDir);
let deletedCount = 0;
files.forEach(file => {
if (file.startsWith('startup-') && file.endsWith('.log')) {
const filePath = path.join(this.logsDir, file);
const stats = fs.statSync(filePath);
const age = now - stats.mtimeMs;
if (age > maxAge) {
fs.unlinkSync(filePath);
deletedCount++;
console.log(`[LogWindowManager] Deleted old log: ${file}`);
}
}
});
if (deletedCount > 0) {
console.log(`[LogWindowManager] Cleaned up ${deletedCount} old log file(s)`);
}
} catch (error) {
console.error('[LogWindowManager] Failed to clean old logs:', error);
}
}
/**
* 写入日志到文件
*/
writeToFile(timestamp, type, message) {
try {
const logLine = `[${timestamp}] [${type.toUpperCase()}] ${message}\n`;
fs.appendFileSync(this.logFilePath, logLine, 'utf-8');
} catch (error) {
console.error('[LogWindowManager] Failed to write log to file:', error);
}
}
/**
* 创建日志窗口
*/
createLogWindow() {
const isDev = !process.resourcesPath;
const iconPath = isDev
? path.join(__dirname, '..', 'public', 'images', 'icon.png')
: path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'icon.png');
this.logWindow = new BrowserWindow({
width: 900,
height: 600,
title: 'NPQS9100 - 服务日志',
backgroundColor: '#1e1e1e',
icon: iconPath,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
@@ -207,7 +298,10 @@ class LogWindowManager {
* 添加日志
*/
addLog(type, message) {
const timestamp = new Date().toLocaleTimeString();
const now = new Date();
const timestamp = now.toLocaleTimeString();
const fullTimestamp = now.toISOString().replace('T', ' ').substring(0, 19);
const logEntry = {
timestamp,
type,
@@ -221,6 +315,9 @@ class LogWindowManager {
this.logs.shift();
}
// 写入文件(使用完整时间戳)
this.writeToFile(fullTimestamp, type, message);
// 发送到窗口
if (this.logWindow && !this.logWindow.isDestroyed()) {
this.logWindow.webContents.send('log-message', logEntry);

View File

@@ -1,373 +0,0 @@
const { spawn, exec } = require('child_process');
const path = require('path');
const fs = require('fs');
class MySQLManager {
constructor() {
// 在开发与打包后均可解析到应用根目录下的 mysql 目录
// 开发环境:项目根目录
// 打包后应用根目录win-unpacked
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')
: path.dirname(process.resourcesPath);
this.mysqlPath = path.join(baseDir, 'mysql');
this.binPath = path.join(this.mysqlPath, 'bin');
this.dataPath = path.join(this.mysqlPath, 'data');
this.process = null;
this.currentPort = null;
}
// 检查MySQL是否已初始化
isInitialized() {
return fs.existsSync(this.dataPath) && fs.readdirSync(this.dataPath).length > 0;
}
// 初始化MySQL数据库
async initialize() {
if (this.isInitialized()) {
console.log('MySQL already initialized');
return Promise.resolve();
}
return new Promise(async (resolve, reject) => {
const mysqld = path.join(this.binPath, 'mysqld.exe');
// 创建初始化SQL文件授权127.0.0.1和所有主机)
const initSqlPath = path.join(this.mysqlPath, 'init_grant.sql');
const initSql = `
CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY '';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION;
CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY '';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
`;
try {
fs.writeFileSync(initSqlPath, initSql, 'utf-8');
console.log('[MySQL] Created init SQL file for granting permissions');
} catch (error) {
console.error('[MySQL] Failed to create init SQL file:', error);
}
// 使用 --init-file 参数初始化并授权
const initProcess = spawn(mysqld, [
'--initialize-insecure',
`--init-file=${initSqlPath}`
], {
cwd: this.mysqlPath,
stdio: 'inherit'
});
initProcess.on('close', (code) => {
if (code === 0) {
console.log('[MySQL] Initialized successfully with permissions granted');
// 删除临时SQL文件
try {
if (fs.existsSync(initSqlPath)) {
fs.unlinkSync(initSqlPath);
}
} catch (e) {
// 忽略删除失败
}
resolve();
} else {
reject(new Error(`MySQL initialization failed with code ${code}`));
}
});
});
}
// 启动MySQL服务
start(port = 3306) {
return new Promise(async (resolve, reject) => {
try {
// 确保数据库已初始化
await this.initialize();
const mysqld = path.join(this.binPath, 'mysqld.exe');
// 启动MySQL指定端口
this.process = spawn(mysqld, [
'--console',
`--port=${port}`
], {
cwd: this.mysqlPath,
stdio: ['ignore', 'pipe', 'pipe']
});
this.currentPort = port;
// 将当前端口写入文件,供停止脚本使用
try {
const portFilePath = path.join(this.mysqlPath, '.running-port');
fs.writeFileSync(portFilePath, port.toString(), 'utf-8');
console.log(`[MySQL] Port ${port} recorded to ${portFilePath}`);
} catch (error) {
console.warn('[MySQL] Failed to record port:', error);
}
let output = '';
this.process.stdout.on('data', (data) => {
output += data.toString();
console.log('MySQL:', data.toString());
// MySQL启动完成的标志
if (output.includes('ready for connections') || output.includes('MySQL Community Server')) {
console.log(`MySQL started successfully on port ${port}`);
// 自动授权 root 用户从任何主机连接
setTimeout(async () => {
try {
console.log('[MySQL] Waiting 3 seconds before granting permissions...');
await this.grantRootAccess();
} catch (error) {
console.warn('[MySQL] Failed to grant root access, but MySQL is running:', error.message);
}
resolve(port);
}, 3000);
}
});
this.process.stderr.on('data', (data) => {
console.error('MySQL Error:', data.toString());
});
this.process.on('close', (code) => {
console.log(`MySQL process exited with code ${code}`);
this.process = null;
this.currentPort = null;
// 删除端口记录文件
try {
const portFilePath = path.join(this.mysqlPath, '.running-port');
if (fs.existsSync(portFilePath)) {
fs.unlinkSync(portFilePath);
console.log('[MySQL] Port record file removed');
}
} catch (error) {
console.warn('[MySQL] Failed to remove port record:', error);
}
});
// 超时处理
setTimeout(async () => {
if (this.process && !this.process.killed) {
console.log(`MySQL started on port ${port} (timeout reached, assuming success)`);
// 自动授权 root 用户从任何主机连接
try {
console.log('[MySQL] Granting permissions...');
await this.grantRootAccess();
} catch (error) {
console.warn('[MySQL] Failed to grant root access, but MySQL is running:', error.message);
}
resolve(port);
}
}, 18000);
} catch (error) {
reject(error);
}
});
}
// 授权 root 用户从 127.0.0.1 访问
grantRootAccess() {
return new Promise((resolve, reject) => {
// 创建 SQL 文件
const sqlFilePath = path.join(this.mysqlPath, 'grant_root.sql');
const sqlContent = `
CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY '';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION;
CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY '';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
`;
try {
fs.writeFileSync(sqlFilePath, sqlContent, 'utf-8');
} catch (error) {
console.error('[MySQL] Failed to create grant SQL file:', error);
return resolve(); // 继续启动
}
const mysqlExe = path.join(this.binPath, 'mysql.exe');
const grantProcess = spawn(mysqlExe, [
'--host=localhost',
`--port=${this.currentPort}`,
'--user=root'
], {
cwd: this.mysqlPath,
stdio: ['pipe', 'pipe', 'pipe']
});
// 通过 stdin 输入 SQL
grantProcess.stdin.write(sqlContent);
grantProcess.stdin.end();
let output = '';
let errorOutput = '';
grantProcess.stdout.on('data', (data) => {
output += data.toString();
});
grantProcess.stderr.on('data', (data) => {
errorOutput += data.toString();
});
grantProcess.on('close', (code) => {
if (code === 0) {
console.log('[MySQL] Root user granted access from 127.0.0.1 and all hosts');
resolve();
} else {
console.error('[MySQL] Grant access failed (code:', code, ')');
console.error('[MySQL] Error output:', errorOutput);
console.error('[MySQL] Standard output:', output);
// 即使失败也 resolve让应用继续启动
resolve();
}
});
});
}
// 获取当前MySQL端口
getCurrentPort() {
return this.currentPort || 3306;
}
// 停止MySQL服务
stop() {
return new Promise(async (resolve) => {
if (this.process && !this.process.killed) {
console.log('[MySQL] Stopping MySQL...');
// 方法1: 尝试使用 mysqladmin shutdown 优雅关闭
try {
console.log('[MySQL] Trying mysqladmin shutdown...');
const mysqladmin = path.join(this.binPath, 'mysqladmin.exe');
if (fs.existsSync(mysqladmin)) {
const shutdownProcess = spawn(mysqladmin, [
'-u', 'root',
'-pnjcnpqs',
'--port=' + this.currentPort,
'shutdown'
], {
cwd: this.mysqlPath,
stdio: 'ignore'
});
// 等待 mysqladmin 执行完成最多5秒
const shutdownPromise = new Promise((res) => {
shutdownProcess.on('close', (code) => {
console.log(`[MySQL] mysqladmin shutdown exited with code ${code}`);
res(code === 0);
});
});
const timeoutPromise = new Promise((res) => setTimeout(() => res(false), 5000));
const shutdownSuccess = await Promise.race([shutdownPromise, timeoutPromise]);
if (shutdownSuccess) {
console.log('[MySQL] Shutdown successful via mysqladmin');
// 等待进程真正退出
await new Promise((res) => {
if (this.process && !this.process.killed) {
this.process.on('close', res);
setTimeout(res, 2000); // 最多等2秒
} else {
res();
}
});
this.cleanupPortFile();
return resolve();
}
}
} catch (error) {
console.warn('[MySQL] mysqladmin shutdown failed:', error.message);
}
// 方法2: 如果 mysqladmin 失败,尝试 SIGTERM
console.log('[MySQL] Trying SIGTERM...');
const killTimeout = setTimeout(() => {
// 方法3: 5秒后强制 SIGKILL
console.log('[MySQL] Force killing with SIGKILL');
try {
if (this.process && !this.process.killed) {
this.process.kill('SIGKILL');
}
} catch (e) {
console.error('[MySQL] Error force killing:', e);
}
this.cleanupPortFile();
resolve();
}, 5000);
this.process.on('close', () => {
clearTimeout(killTimeout);
console.log('[MySQL] Process closed');
this.cleanupPortFile();
resolve();
});
try {
this.process.kill('SIGTERM');
} catch (e) {
console.error('[MySQL] Error sending SIGTERM:', e);
clearTimeout(killTimeout);
this.cleanupPortFile();
resolve();
}
} else {
// 没有进程引用说明MySQL已经停止或不在我们控制下
console.log('[MySQL] No process reference, MySQL may already be stopped');
console.log('[MySQL] If MySQL is still running, please use kill-running-port.bat to clean up');
this.cleanupPortFile();
resolve();
}
});
}
// 清理端口记录文件
cleanupPortFile() {
try {
const portFilePath = path.join(this.mysqlPath, '.running-port');
if (fs.existsSync(portFilePath)) {
fs.unlinkSync(portFilePath);
console.log('[MySQL] Port record file cleaned up');
}
} catch (error) {
console.warn('[MySQL] Failed to cleanup port record:', error);
}
}
// 获取记录的运行端口
getRecordedPort() {
try {
const portFilePath = path.join(this.mysqlPath, '.running-port');
if (fs.existsSync(portFilePath)) {
const port = fs.readFileSync(portFilePath, 'utf-8').trim();
return parseInt(port);
}
} catch (error) {
console.warn('[MySQL] Failed to read port record:', error);
}
return null;
}
// 获取MySQL连接配置
getConnectionConfig() {
return {
host: 'localhost',
port: 3306,
user: 'root',
password: '',
database: 'app_db'
};
}
}
module.exports = MySQLManager;

View File

@@ -10,9 +10,8 @@ class StartupManager {
this.loadingWindow = null;
this.steps = [
{ id: 'init', label: '正在初始化应用...', progress: 0 },
{ id: 'check-mysql-port', label: '正在检MySQL端口...', progress: 15 },
{ id: 'start-mysql', label: '正在启动MySQL数据库...', progress: 30 },
{ id: 'wait-mysql', label: '等待MySQL就绪...', progress: 45 },
{ id: 'check-mysql-port', label: '正在检MySQL服务...', progress: 20 },
{ id: 'wait-mysql', label: '确保MySQL服务运行...', progress: 40 },
{ id: 'check-java-port', label: '正在检测后端服务端口...', progress: 60 },
{ id: 'generate-config', label: '正在生成配置文件...', progress: 70 },
{ id: 'start-java', label: '正在启动后端服务...', progress: 80 },
@@ -26,6 +25,11 @@ class StartupManager {
* 创建 Loading 窗口
*/
createLoadingWindow() {
const isDev = !process.resourcesPath;
const iconPath = isDev
? path.join(__dirname, '..', 'public', 'images', 'icon.png')
: path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'icon.png');
this.loadingWindow = new BrowserWindow({
width: 500,
height: 300,
@@ -34,6 +38,7 @@ class StartupManager {
resizable: false,
alwaysOnTop: true,
skipTaskbar: true, // 不在任务栏显示
icon: iconPath,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
@@ -41,7 +46,12 @@ class StartupManager {
});
// 加载 loading 页面
const loadingHtml = path.join(__dirname, '../public/html/loading.html');
const isDev2 = !process.resourcesPath;
const loadingHtml = isDev2
? path.join(__dirname, '..', 'public', 'html', 'loading.html')
: path.join(process.resourcesPath, 'app.asar', 'public', 'html', 'loading.html');
console.log('[StartupManager] Loading HTML from:', loadingHtml);
this.loadingWindow.loadFile(loadingHtml);
return this.loadingWindow;