From 6f046582657527218154330cb847acaed980bbca Mon Sep 17 00:00:00 2001 From: zhangzijienbplus <17738440858@163.com> Date: Wed, 15 Oct 2025 18:32:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(client):=20=E8=AE=BE=E5=A4=87=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E9=80=BB=E8=BE=91=E4=B8=8E=E8=AE=A4=E8=AF=81=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改设备移除时的本地清理方法,统一调用 clearLocalAuth - 优化设备数量限制校验逻辑,避免重复计算当前设备- 移除冗余的设备状态检查,简化设备移除流程- 调整 Redis 连接超时与等待时间,提升连接稳定性- 增强 MySQL 数据库连接配置,添加自动重连机制 -优化 Druid 连接池参数,提高数据库连接性能 - 简化客户端认证与数据上报逻辑,提升处理效率 - 移除过期设备状态更新逻辑,减少不必要的数据库操作- 调整慢 SQL 记录阈值,便于及时发现性能问题-优化版本分布与数据类型统计查询逻辑,提高响应速度 --- electron-vue-template/electron-builder.json | 85 +--- electron-vue-template/package.json | 1 + .../public/config/logback.xml | 52 ++ electron-vue-template/src/main/main.ts | 355 ++++++------- electron-vue-template/src/renderer/App.vue | 4 +- .../src/renderer/api/device.ts | 5 - .../components/common/UpdateDialog.vue | 122 +++-- electron-vue-template/update-helper.bat | 101 ++-- .../tashow/erp/ErpClientSbApplication.java | 2 - .../tashow/erp/test/SeleniumWithProfile.java | 3 - .../com/tashow/erp/utils/DeviceUtils.java | 22 +- erp_client_sb/src/main/resources/logback.xml | 36 +- .../monitor/ClientAccountController.java | 83 ++- .../controller/monitor/VersionController.java | 31 +- .../system/ClientDeviceController.java | 28 +- .../web/controller/tool/FileController.java | 1 - .../web/service/IClientMonitorService.java | 50 -- .../impl/ClientMonitorServiceImpl.java | 480 ++---------------- .../java/com/ruoyi/web/sse/SseHubService.java | 24 +- .../ruoyi/web/task/DeviceHeartbeatTask.java | 8 - .../src/main/resources/application-druid.yml | 28 +- .../src/main/resources/application.yml | 4 +- .../main/resources/mybatis/mybatis-config.xml | 4 +- .../system/mapper/ClientDeviceMapper.java | 5 +- .../system/mapper/ClientMonitorMapper.java | 7 - .../mapper/system/ClientDeviceMapper.xml | 7 +- .../mapper/system/ClientMonitorMapper.xml | 8 - ruoyi-ui/src/api/monitor/version.js | 1 + ruoyi-ui/src/views/monitor/version/index.vue | 155 ++++-- 29 files changed, 702 insertions(+), 1010 deletions(-) create mode 100644 electron-vue-template/public/config/logback.xml diff --git a/electron-vue-template/electron-builder.json b/electron-vue-template/electron-builder.json index 26332e8..589d9f0 100644 --- a/electron-vue-template/electron-builder.json +++ b/electron-vue-template/electron-builder.json @@ -9,15 +9,12 @@ "public/jre/**/*", "public/icon/**/*", "public/image/**/*", - "public/splash.html" + "public/splash.html", + "public/config/**/*" ], "directories": { "output": "dist" }, - "publish": { - "provider": "generic", - "url": "http://192.168.1.89:8085/static/updates/" - }, "nsis": { "oneClick": false, "perMachine": false, @@ -28,10 +25,8 @@ "target": "nsis", "icon": "public/icon/icon.png" }, - "linux": { - "target": ["snap"] - }, "files": [ + "package.json", { "from": "build/main", "to": "main", @@ -43,18 +38,10 @@ "filter": ["**/*"] }, { - "from": "src/main/static", "to": "static", "filter": ["**/*"] }, - { - "from": "public", - "to": "assets", - "filter": [ - "erp_client_sb-*.jar" - ] - }, { "from": "public", "to": "public", @@ -63,58 +50,42 @@ "icon/**/*", "image/**/*", "splash.html", + "config/**/*", "!erp_client_sb-*.jar", "!data/**/*", - "!jre/bin/jabswitch.exe", - "!jre/bin/jaccessinspector.exe", - "!jre/bin/jaccesswalker.exe", - "!jre/bin/jar.exe", - "!jre/bin/jarsigner.exe", - "!jre/bin/javac.exe", - "!jre/bin/javadoc.exe", - "!jre/bin/javap.exe", - "!jre/bin/jcmd.exe", - "!jre/bin/jconsole.exe", - "!jre/bin/jdb.exe", - "!jre/bin/jdeprscan.exe", - "!jre/bin/jdeps.exe", - "!jre/bin/jfr.exe", - "!jre/bin/jhsdb.exe", - "!jre/bin/jimage.exe", - "!jre/bin/jinfo.exe", - "!jre/bin/jlink.exe", - "!jre/bin/jmap.exe", - "!jre/bin/jmod.exe", - "!jre/bin/jpackage.exe", - "!jre/bin/jps.exe", - "!jre/bin/jrunscript.exe", - "!jre/bin/jshell.exe", - "!jre/bin/jstack.exe", - "!jre/bin/jstat.exe", - "!jre/bin/jstatd.exe", - "!jre/bin/keytool.exe", - "!jre/bin/kinit.exe", - "!jre/bin/klist.exe", - "!jre/bin/ktab.exe", - "!jre/bin/rmiregistry.exe", - "!jre/bin/serialver.exe", + "!jre/bin/jab*.exe", + "!jre/bin/jac*.exe", + "!jre/bin/jar*.exe", + "!jre/bin/jc*.exe", + "!jre/bin/jd*.exe", + "!jre/bin/jf*.exe", + "!jre/bin/jh*.exe", + "!jre/bin/ji*.exe", + "!jre/bin/jl*.exe", + "!jre/bin/jm*.exe", + "!jre/bin/jp*.exe", + "!jre/bin/jr*.exe", + "!jre/bin/jsh*.exe", + "!jre/bin/jst*.exe", + "!jre/bin/k*.exe", + "!jre/bin/rmi*.exe", + "!jre/bin/serial*.exe", "!jre/include/**", "!jre/lib/src.zip", "!jre/lib/ct.sym", - "!jre/lib/jvm.lib", - "!icon/image.png", - "!icon/img.png" + "!jre/lib/jvm.lib" ] - }, - "!build", - "!dist", - "!scripts" + } ], - "electronLanguages": ["en", "zh-CN"], "extraResources": [ { "from": "update-helper.bat", "to": "../update-helper.bat" + }, + { + "from": "public", + "to": "./", + "filter": ["erp_client_sb-*.jar"] } ] } diff --git a/electron-vue-template/package.json b/electron-vue-template/package.json index 0b63dd5..450e16e 100644 --- a/electron-vue-template/package.json +++ b/electron-vue-template/package.json @@ -5,6 +5,7 @@ "main": "main/main.js", "scripts": { "dev": "node scripts/dev-server.js", + "build": "node scripts/build.js && electron-builder", "build:win": "node scripts/build.js && electron-builder --win", "build:mac": "node scripts/build.js && electron-builder --mac", "build:linux": "node scripts/build.js && electron-builder --linux" diff --git a/electron-vue-template/public/config/logback.xml b/electron-vue-template/public/config/logback.xml new file mode 100644 index 0000000..144f031 --- /dev/null +++ b/electron-vue-template/public/config/logback.xml @@ -0,0 +1,52 @@ + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + UTF-8 + + + + + + ${LOG_HOME}/spring-boot.log + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + UTF-8 + + + ${LOG_HOME}/spring-boot-%d{yyyy-MM-dd}.log + 30 + 1GB + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/electron-vue-template/src/main/main.ts b/electron-vue-template/src/main/main.ts index f6b9f06..40d45fd 100644 --- a/electron-vue-template/src/main/main.ts +++ b/electron-vue-template/src/main/main.ts @@ -1,16 +1,21 @@ import {app, BrowserWindow, ipcMain, Menu, screen, dialog} from 'electron'; -import {existsSync, createWriteStream, promises as fs, mkdirSync, copyFileSync} from 'fs'; -import {join, dirname} from 'path'; +import {existsSync, createWriteStream, promises as fs, mkdirSync, copyFileSync, readdirSync, writeFileSync} from 'fs'; +import {join, dirname, basename} from 'path'; import {spawn, ChildProcess} from 'child_process'; import * as https from 'https'; import * as http from 'http'; + +const isDev = process.env.NODE_ENV === 'development'; + let springProcess: ChildProcess | null = null; let mainWindow: BrowserWindow | null = null; let splashWindow: BrowserWindow | null = null; let appOpened = false; -let downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB', speed: ''}; +let downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'}; let isDownloading = false; let downloadedFilePath: string | null = null; +let downloadedAsarPath: string | null = null; +let downloadedJarPath: string | null = null; function openAppIfNotOpened() { if (appOpened) return; appOpened = true; @@ -20,8 +25,8 @@ function openAppIfNotOpened() { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.show(); mainWindow.focus(); + if (isDev) mainWindow.webContents.openDevTools(); } - // 安全关闭启动画面 if (splashWindow && !splashWindow.isDestroyed()) { splashWindow.close(); splashWindow = null; @@ -29,141 +34,75 @@ function openAppIfNotOpened() { }, 1000); }); - if (process.env.NODE_ENV === 'development') { - const rendererPort = process.argv[2] || 8083; - mainWindow.loadURL(`http://localhost:${rendererPort}`); - } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html')); - } + isDev + ? mainWindow.loadURL(`http://localhost:${process.argv[2] || 8083}`) + : mainWindow.loadFile(join(__dirname, '../renderer/index.html')); } } +// 通用资源路径获取函数 +function getResourcePath(devPath: string, prodPath: string, fallbackPath?: string): string { + if (isDev) return join(__dirname, devPath); + const bundledPath = join(process.resourcesPath, 'app.asar.unpacked', prodPath); + if (existsSync(bundledPath)) return bundledPath; + return fallbackPath ? join(__dirname, fallbackPath) : join(__dirname, prodPath); +} + function getJavaExecutablePath(): string { - if (process.env.NODE_ENV === 'development') { - return 'java'; - } - - const bundledJavaPath = join(process.resourcesPath, 'app.asar.unpacked/public/jre/bin/java.exe'); - if (existsSync(bundledJavaPath)) { - return bundledJavaPath; - } - - const devJavaPath = join(__dirname, '../../public/jre/bin/java.exe'); - if (existsSync(devJavaPath)) { - return devJavaPath; - } - - return 'java'; + if (isDev) return 'java'; + const javaPath = getResourcePath('', 'public/jre/bin/java.exe'); + return existsSync(javaPath) ? javaPath : 'java'; } function findJarFile(directory: string): string { if (!existsSync(directory)) return ''; - - const files = require('fs').readdirSync(directory); - const jarFile = files.find((f: string) => f.startsWith('erp_client_sb-') && f.endsWith('.jar')); - + const jarFile = readdirSync(directory).find((f: string) => f.startsWith('erp_client_sb-') && f.endsWith('.jar')); return jarFile ? join(directory, jarFile) : ''; } -function extractVersionFromJar(jarPath: string): string { - if (!jarPath) return ''; - const match = require('path').basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/); - return match?.[1] || ''; -} - function getJarFilePath(): string { - if (process.env.NODE_ENV === 'development') { - return findJarFile(join(__dirname, '../../public')); - } - - const tempDir = join(app.getPath('temp'), 'erp-client'); - if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true }); - - const asarJarPath = findJarFile(join(__dirname, '../assets')); - if (!asarJarPath) return ''; - - const asarFileName = require('path').basename(asarJarPath); - const tempJarPath = join(tempDir, asarFileName); - - // 如果临时目录版本不同,删除旧版本并复制新版本 - const existingJar = findJarFile(tempDir); - if (existingJar && require('path').basename(existingJar) !== asarFileName) { - require('fs').unlinkSync(existingJar); - } - - if (!existsSync(tempJarPath)) { - copyFileSync(asarJarPath, tempJarPath); - } - - return tempJarPath; + if (isDev) return findJarFile(join(__dirname, '../../public')); + return findJarFile(process.resourcesPath); } -function getSplashPath(): string { - if (process.env.NODE_ENV === 'development') { - return join(__dirname, '../../public/splash.html'); - } - - const bundledSplashPath = join(process.resourcesPath, 'app.asar.unpacked/public/splash.html'); - if (existsSync(bundledSplashPath)) { - return bundledSplashPath; - } - - return join(__dirname, '../../public/splash.html'); -} -function getIconPath(): string { - if (process.env.NODE_ENV === 'development') { - return join(__dirname, '../../public/icon/icon.png'); - } - const bundledIconPath = join(process.resourcesPath, 'app.asar.unpacked/public/icon/icon.png'); - if (existsSync(bundledIconPath)) { - return bundledIconPath; - } - - return join(__dirname, '../renderer/icon/icon.png'); -} +const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/splash.html'); +const getIconPath = () => getResourcePath('../../public/icon/icon.png', 'public/icon/icon.png', '../renderer/icon/icon.png'); +const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml'); function getDataDirectoryPath(): string { - // 将用户数据目录放在可写的应用数据目录下 - const userDataPath = app.getPath('userData'); - const dataDir = join(userDataPath, 'data'); - - // 确保数据目录存在 - if (!existsSync(dataDir)) { - mkdirSync(dataDir, { recursive: true }); - } - + const dataDir = join(app.getPath('userData'), 'data'); + if (!existsSync(dataDir)) mkdirSync(dataDir, {recursive: true}); return dataDir; } function migrateDataFromPublic(): void { - // 如果是首次运行,尝试从public/data迁移数据 + if (!isDev) return; + const oldDataPath = join(__dirname, '../../public/data'); + if (!existsSync(oldDataPath)) return; + const newDataPath = getDataDirectoryPath(); - - if (process.env.NODE_ENV === 'development' && existsSync(oldDataPath)) { - try { - const files = require('fs').readdirSync(oldDataPath); - for (const file of files) { - const srcFile = join(oldDataPath, file); - const destFile = join(newDataPath, file); - - if (!existsSync(destFile)) { - require('fs').copyFileSync(srcFile, destFile); - } + try { + readdirSync(oldDataPath).forEach(file => { + const destFile = join(newDataPath, file); + if (!existsSync(destFile)) { + copyFileSync(join(oldDataPath, file), destFile); } - } catch (error) { - console.log('数据迁移失败,使用默认配置'); - } + }); + } catch (error) { + console.log('数据迁移失败,使用默认配置'); } } + function startSpringBoot() { migrateDataFromPublic(); const jarPath = getJarFilePath(); const javaPath = getJavaExecutablePath(); const dataDir = getDataDirectoryPath(); - + const logbackConfigPath = getLogbackConfigPath(); + if (!existsSync(jarPath)) { dialog.showErrorBox('启动失败', `JAR 文件不存在:\n${jarPath}`); app.quit(); @@ -175,19 +114,14 @@ function startSpringBoot() { const springArgs = [ '-jar', jarPath, `--spring.datasource.url=jdbc:sqlite:${dataDir}/erp-cache.db`, - `--logging.file.path=${dataDir}`, - `--server.port=8081` + `--server.port=8081`, + `--logging.config=file:${logbackConfigPath}` ]; // 工作目录设为数据目录,这样Spring Boot会在数据目录下创建临时文件 springProcess = spawn(javaPath, springArgs, { cwd: dataDir, - detached: false, - env: { - ...process.env, - 'ERP_DATA_DIR': dataDir, - 'USER_DATA_DIR': dataDir - } + detached: false }); let startupCompleted = false; @@ -202,6 +136,16 @@ function startSpringBoot() { } }); + springProcess.stderr?.on('data', (data) => { + const output = data.toString(); + console.log('[Spring Boot]', output.trim()); + + if (!startupCompleted && (output.includes('Started Success') || output.includes('Started ErpClientSbApplication'))) { + startupCompleted = true; + openAppIfNotOpened(); + } + }); + springProcess.on('close', (code) => { mainWindow ? mainWindow.close() : app.quit(); }); @@ -226,7 +170,9 @@ function startSpringBoot() { app.quit(); } } - startSpringBoot(); + +startSpringBoot(); + function stopSpringBoot() { if (!springProcess) return; try { @@ -264,11 +210,6 @@ function createWindow() { Menu.setApplicationMenu(null); mainWindow.setMenuBarVisibility(false); - // 打开开发者工具 - if (process.env.NODE_ENV === 'development') { - mainWindow.webContents.openDevTools(); - } - // 监听窗口关闭事件,确保正确清理引用 mainWindow.on('closed', () => { mainWindow = null; @@ -334,14 +275,29 @@ ipcMain.on('message', (event, message) => { console.log(message); }); -ipcMain.handle('get-jar-version', () => extractVersionFromJar(getJarFilePath())); +ipcMain.handle('get-jar-version', () => { + const jarPath = getJarFilePath(); + const match = jarPath ? basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/) : null; + return match?.[1] || ''; +}); function checkPendingUpdate() { try { - const updateFilePath = join(process.resourcesPath, 'app.asar.update'); + const asarUpdatePath = join(process.resourcesPath, 'app.asar.update'); const appAsarPath = join(process.resourcesPath, 'app.asar'); - if (!existsSync(updateFilePath)) return; + // 查找jar更新文件 + let jarUpdatePath = ''; + if (process.resourcesPath && existsSync(process.resourcesPath)) { + const files = readdirSync(process.resourcesPath); + const jarUpdateFile = files.find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); + if (jarUpdateFile) { + jarUpdatePath = join(process.resourcesPath, jarUpdateFile); + } + } + + // 如果没有任何更新文件,直接返回 + if (!existsSync(asarUpdatePath) && !jarUpdatePath) return; const appDir = dirname(process.execPath); const helperPath = join(appDir, 'update-helper.bat'); @@ -353,9 +309,9 @@ function checkPendingUpdate() { const vbsPath = join(app.getPath('temp'), 'update-silent.vbs'); const vbsContent = `Set WshShell = CreateObject("WScript.Shell") -WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${updateFilePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`; +WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${asarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${jarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`; - require('fs').writeFileSync(vbsPath, vbsContent); + writeFileSync(vbsPath, vbsContent); spawn('wscript.exe', [vbsPath], { detached: true, @@ -369,53 +325,86 @@ WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & } } -ipcMain.handle('download-update', async (event, downloadUrl: string) => { +ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string, jarUrl?: string}) => { if (isDownloading) return {success: false, error: '正在下载中'}; if (downloadedFilePath === 'completed') return {success: true, filePath: 'already-completed'}; isDownloading = true; + let totalDownloaded = 0; + let totalSize = 0; try { - const tempPath = join(app.getPath('temp'), 'app.asar.new'); + // 获取总大小 + const sizes = await Promise.all([ + downloadUrls.asarUrl ? getFileSize(downloadUrls.asarUrl) : 0, + downloadUrls.jarUrl ? getFileSize(downloadUrls.jarUrl) : 0 + ]); + totalSize = sizes[0] + sizes[1]; - await downloadFile(downloadUrl, tempPath, (progress) => { - downloadProgress = progress; - if (mainWindow) { - mainWindow.webContents.send('download-progress', progress); + // 下载asar文件 + if (downloadUrls.asarUrl) { + const tempAsarPath = join(app.getPath('temp'), 'app.asar.new'); + const asarSize = sizes[0]; + + await downloadFile(downloadUrls.asarUrl, tempAsarPath, (progress) => { + const combinedProgress = { + percentage: Math.round(((totalDownloaded + progress.downloaded) / totalSize) * 100), + current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`, + total: `${(totalSize / 1024 / 1024).toFixed(1)} MB` + }; + downloadProgress = combinedProgress; + if (mainWindow) mainWindow.webContents.send('download-progress', combinedProgress); + }); + + const asarUpdatePath = join(process.resourcesPath, 'app.asar.update'); + await fs.copyFile(tempAsarPath, asarUpdatePath); + await fs.unlink(tempAsarPath); + downloadedAsarPath = asarUpdatePath; + totalDownloaded += asarSize; + } + + // 下载jar文件 + if (downloadUrls.jarUrl) { + let jarFileName = basename(downloadUrls.jarUrl); + if (!jarFileName.match(/^erp_client_sb-[\d.]+\.jar$/)) { + const currentJar = getJarFilePath(); + const versionMatch = currentJar ? basename(currentJar).match(/erp_client_sb-([\d.]+)\.jar/) : null; + if (versionMatch && versionMatch[1]) { + const versionParts = versionMatch[1].split('.'); + versionParts[2] = String(Number(versionParts[2]) + 1); + jarFileName = `erp_client_sb-${versionParts.join('.')}.jar`; + } else { + jarFileName = 'erp_client_sb-2.4.7.jar'; + } } - }); + + const tempJarPath = join(app.getPath('temp'), jarFileName); - if (!existsSync(tempPath)) { - throw new Error('下载文件不存在'); - } + await downloadFile(downloadUrls.jarUrl, tempJarPath, (progress) => { + const combinedProgress = { + percentage: Math.round(((totalDownloaded + progress.downloaded) / totalSize) * 100), + current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`, + total: `${(totalSize / 1024 / 1024).toFixed(1)} MB` + }; + downloadProgress = combinedProgress; + if (mainWindow) mainWindow.webContents.send('download-progress', combinedProgress); + }); - const fileStats = await fs.stat(tempPath); - if (fileStats.size < 1000) { - throw new Error('下载文件过小'); - } - - const updateFilePath = join(process.resourcesPath, 'app.asar.update'); - await fs.copyFile(tempPath, updateFilePath); - await fs.unlink(tempPath); - - if (!existsSync(updateFilePath)) { - throw new Error('更新文件保存失败'); + const jarUpdatePath = join(process.resourcesPath, jarFileName + '.update'); + await fs.copyFile(tempJarPath, jarUpdatePath); + await fs.unlink(tempJarPath); + downloadedJarPath = jarUpdatePath; } downloadedFilePath = 'completed'; isDownloading = false; - return {success: true, filePath: updateFilePath}; + return {success: true, asarPath: downloadedAsarPath, jarPath: downloadedJarPath}; } catch (error: unknown) { isDownloading = false; downloadedFilePath = null; - - const tempPath = join(app.getPath('temp'), 'app.asar.new'); - if (existsSync(tempPath)) { - fs.unlink(tempPath).catch(() => { - }); - } - + downloadedAsarPath = null; + downloadedJarPath = null; return {success: false, error: error instanceof Error ? error.message : '下载失败'}; } }); @@ -426,26 +415,37 @@ ipcMain.handle('get-download-progress', () => { ipcMain.handle('install-update', async () => { try { - const updateFilePath = join(process.resourcesPath, 'app.asar.update'); - const hasUpdateFile = existsSync(updateFilePath); + const asarUpdatePath = join(process.resourcesPath, 'app.asar.update'); + const hasAsarUpdate = existsSync(asarUpdatePath); + + // 查找jar更新文件 + let jarUpdatePath = ''; + if (process.resourcesPath && existsSync(process.resourcesPath)) { + const files = readdirSync(process.resourcesPath); + const jarUpdateFile = files.find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); + if (jarUpdateFile) { + jarUpdatePath = join(process.resourcesPath, jarUpdateFile); + } + } - if (!downloadedFilePath || (downloadedFilePath !== 'completed' && !hasUpdateFile)) { + if (!hasAsarUpdate && !jarUpdatePath) { return {success: false, error: '更新文件不存在'}; } const appDir = dirname(process.execPath); const helperPath = join(appDir, 'update-helper.bat'); - const appAsarPath = join(process.resourcesPath, 'app.asar'); if (!existsSync(helperPath)) { return {success: false, error: '更新助手不存在'}; } + const appAsarPath = join(process.resourcesPath, 'app.asar'); + const vbsPath = join(app.getPath('temp'), 'update-install.vbs'); const vbsContent = `Set WshShell = CreateObject("WScript.Shell") -WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${updateFilePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`; +WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${asarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${jarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`; - require('fs').writeFileSync(vbsPath, vbsContent); + writeFileSync(vbsPath, vbsContent); spawn('wscript.exe', [vbsPath], { detached: true, @@ -455,6 +455,8 @@ WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & setTimeout(() => { downloadedFilePath = null; + downloadedAsarPath = null; + downloadedJarPath = null; app.quit(); }, 500); @@ -466,8 +468,10 @@ WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & ipcMain.handle('cancel-download', () => { isDownloading = false; - downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB', speed: ''}; + downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'}; downloadedFilePath = null; + downloadedAsarPath = null; + downloadedJarPath = null; return {success: true}; }); @@ -492,7 +496,17 @@ ipcMain.handle('write-file', async (event, filePath: string, data: Uint8Array) = }); -async function downloadFile(url: string, filePath: string, onProgress: (progress: any) => void): Promise { +async function getFileSize(url: string): Promise { + return new Promise((resolve, reject) => { + const protocol = url.startsWith('https') ? https : http; + protocol.get(url, {method: 'HEAD'}, (response) => { + const size = parseInt(response.headers['content-length'] || '0', 10); + resolve(size); + }).on('error', () => resolve(0)); + }); +} + +async function downloadFile(url: string, filePath: string, onProgress: (progress: {downloaded: number}) => void): Promise { return new Promise((resolve, reject) => { const protocol = url.startsWith('https') ? https : http; @@ -502,22 +516,12 @@ async function downloadFile(url: string, filePath: string, onProgress: (progress return; } - const totalBytes = parseInt(response.headers['content-length'] || '0', 10); let downloadedBytes = 0; - const startTime = Date.now(); - const fileStream = createWriteStream(filePath); response.on('data', (chunk) => { downloadedBytes += chunk.length; - - const percentage = totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : 0; - const current = `${(downloadedBytes / 1024 / 1024).toFixed(1)} MB`; - const total = `${(totalBytes / 1024 / 1024).toFixed(1)} MB`; - const elapsed = (Date.now() - startTime) / 1000; - const speed = elapsed > 0 ? `${((downloadedBytes / elapsed) / 1024 / 1024).toFixed(1)} MB/s` : ''; - - onProgress({percentage, current, total, speed}); + onProgress({downloaded: downloadedBytes}); }); response.pipe(fileStream); @@ -528,8 +532,7 @@ async function downloadFile(url: string, filePath: string, onProgress: (progress }); fileStream.on('error', (error) => { - fs.unlink(filePath).catch(() => { - }); + fs.unlink(filePath).catch(() => {}); reject(error); }); diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index 0374324..a74310e 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -304,7 +304,7 @@ const SSEManager = { console.log('SSE连接已就绪') break case 'DEVICE_REMOVED': - logout() + clearLocalAuth() ElMessage.warning('您的设备已被移除,请重新登录') break case 'FORCE_LOGOUT': @@ -390,7 +390,7 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) { deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1) if (row.deviceId === getClientIdFromToken()) { - clearLocalAuth() // 移除当前设备:只清理本地状态,不调用offline(避免覆盖removed状态) + clearLocalAuth() } ElMessage.success('已移除设备') diff --git a/electron-vue-template/src/renderer/api/device.ts b/electron-vue-template/src/renderer/api/device.ts index c7aae2d..f113e34 100644 --- a/electron-vue-template/src/renderer/api/device.ts +++ b/electron-vue-template/src/renderer/api/device.ts @@ -21,11 +21,6 @@ export const deviceApi = { return http.post('/monitor/device/remove', payload) }, - heartbeat(payload: { username: string; deviceId: string; version?: string }) { - // 直接调用 RuoYi 后端的心跳接口 - return http.post('/monitor/device/heartbeat', payload) - }, - offline(payload: { deviceId: string }) { // 直接调用 RuoYi 后端的离线接口 return http.post('/monitor/device/offline', payload) diff --git a/electron-vue-template/src/renderer/components/common/UpdateDialog.vue b/electron-vue-template/src/renderer/components/common/UpdateDialog.vue index 61da0ef..dd9ba63 100644 --- a/electron-vue-template/src/renderer/components/common/UpdateDialog.vue +++ b/electron-vue-template/src/renderer/components/common/UpdateDialog.vue @@ -4,7 +4,8 @@ v{{ version || '-' }} -
@@ -48,7 +49,7 @@
-

正在下载更新

+

正在下载安装...

-
- App Icon -

更新完成

-

更新文件已下载,将在重启后自动应用

-
- -
-
- {{ prog.current }} / {{ prog.total }} +
+
+ App Icon +
+
+
+

可以开始安装了

+
+
+ +
+ {{ prog.current }} / {{ prog.total }} + 下载完成 +
+ 立即重启 +
+
+
- -
- -
- 稍后更新 - 重启应用新版本
@@ -92,7 +96,7 @@