diff --git a/electron-vue-template/package.json b/electron-vue-template/package.json index 450e16e..0b63dd5 100644 --- a/electron-vue-template/package.json +++ b/electron-vue-template/package.json @@ -5,7 +5,6 @@ "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/src/main/main.ts b/electron-vue-template/src/main/main.ts index 086dd45..f6b9f06 100644 --- a/electron-vue-template/src/main/main.ts +++ b/electron-vue-template/src/main/main.ts @@ -21,7 +21,6 @@ function openAppIfNotOpened() { mainWindow.show(); mainWindow.focus(); } - // 安全关闭启动画面 if (splashWindow && !splashWindow.isDestroyed()) { splashWindow.close(); @@ -227,7 +226,7 @@ function startSpringBoot() { app.quit(); } } -startSpringBoot(); + startSpringBoot(); function stopSpringBoot() { if (!springProcess) return; try { diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index 4ee8d4b..0374324 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -3,7 +3,7 @@ import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmoun import {ElMessage, ElMessageBox} from 'element-plus' import zhCn from 'element-plus/es/locale/lang/zh-cn' import 'element-plus/dist/index.css' -import {authApi} from './api/auth' +import {authApi, TOKEN_KEY} from './api/auth' import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device' import {getOrCreateDeviceId} from './utils/deviceId' const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue')) @@ -146,21 +146,19 @@ function handleMenuSelect(key: string) { } async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) { - isAuthenticated.value = true - showAuthDialog.value = false - showRegDialog.value = false - try { - await authApi.saveToken(data.token) - const username = getUsernameFromToken(data.token) - currentUsername.value = username - userPermissions.value = data?.permissions || '' - vipExpireTime.value = data?.expireTime ? new Date(data.expireTime) : null + localStorage.setItem(TOKEN_KEY, data.token) + isAuthenticated.value = true + showAuthDialog.value = false + showRegDialog.value = false + + currentUsername.value = getUsernameFromToken(data.token) + userPermissions.value = data.permissions || '' + vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null - // 获取设备ID并注册设备 const deviceId = await getOrCreateDeviceId() await deviceApi.register({ - username, + username: currentUsername.value, deviceId, os: navigator.platform }) @@ -168,26 +166,13 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e } catch (e: any) { isAuthenticated.value = false showAuthDialog.value = true - await authApi.deleteTokenCache() + localStorage.removeItem(TOKEN_KEY) ElMessage.error(e?.message || '设备注册失败') } } -async function logout() { - try { - const deviceId = await getClientIdFromToken() - if (deviceId) await deviceApi.offline({ deviceId }) - } catch (error) { - console.warn('离线通知失败:', error) - } - - try { - const tokenRes: any = await authApi.getToken() - const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data - if (token) await authApi.logout(token) - } catch {} - - await authApi.deleteTokenCache() +function clearLocalAuth() { + localStorage.removeItem(TOKEN_KEY) isAuthenticated.value = false currentUsername.value = '' userPermissions.value = '' @@ -197,6 +182,16 @@ async function logout() { SSEManager.disconnect() } +async function logout() { + try { + const deviceId = getClientIdFromToken() + if (deviceId) await deviceApi.offline({ deviceId }) + } catch (error) { + console.warn('离线通知失败:', error) + } + clearLocalAuth() +} + async function handleUserClick() { if (!isAuthenticated.value) { showAuthDialog.value = true @@ -220,47 +215,42 @@ function showRegisterDialog() { function backToLogin() { showRegDialog.value = false + showAuthDialog.value = true } async function checkAuth() { try { - await authApi.sessionBootstrap().catch(() => undefined) - const tokenRes: any = await authApi.getToken() - const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data - - if (token) { - const verifyRes: any = await authApi.verifyToken(token) - isAuthenticated.value = true - currentUsername.value = getUsernameFromToken(token) || '' - - userPermissions.value = verifyRes?.data?.permissions || verifyRes?.permissions || '' - if (verifyRes?.data?.expireTime || verifyRes?.expireTime) { - vipExpireTime.value = new Date(verifyRes?.data?.expireTime || verifyRes?.expireTime) + const token = localStorage.getItem(TOKEN_KEY) + if (!token) { + if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) { + showAuthDialog.value = true } - - SSEManager.connect() return } - } catch { - await authApi.deleteTokenCache() - } - if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) { - showAuthDialog.value = true + const res: any = await authApi.verifyToken(token) + isAuthenticated.value = true + currentUsername.value = getUsernameFromToken(token) || '' + userPermissions.value = res?.data?.permissions || res?.permissions || '' + + const expireTime = res?.data?.expireTime || res?.expireTime + if (expireTime) vipExpireTime.value = new Date(expireTime) + + SSEManager.connect() + } catch { + localStorage.removeItem(TOKEN_KEY) + if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) { + showAuthDialog.value = true + } } } -async function getClientIdFromToken(token?: string) { +function getClientIdFromToken(token?: string) { try { - let t = token - if (!t) { - const tokenRes: any = await authApi.getToken() - t = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data - } + const t = token || localStorage.getItem(TOKEN_KEY) if (!t) return '' - const payload = JSON.parse(atob(t.split('.')[1])) - return payload.clientId || '' + return JSON.parse(atob(t.split('.')[1])).clientId || '' } catch { return '' } @@ -268,32 +258,23 @@ async function getClientIdFromToken(token?: string) { function getUsernameFromToken(token: string) { try { - const payload = JSON.parse(atob(token.split('.')[1])) - return payload.username || '' + return JSON.parse(atob(token.split('.')[1])).username || '' } catch { return '' } } -// SSE管理器 const SSEManager = { connection: null as EventSource | null, async connect() { if (this.connection) return try { - const tokenRes: any = await authApi.getToken() - const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data - if (!token) { - console.warn('SSE连接失败: 没有有效的 token') - return - } + const token = localStorage.getItem(TOKEN_KEY) + if (!token) return console.warn('SSE连接失败: 没有token') - const clientId = await getClientIdFromToken(token) - if (!clientId) { - console.warn('SSE连接失败: 无法从 token 获取 clientId') - return - } + const clientId = getClientIdFromToken(token) + if (!clientId) return console.warn('SSE连接失败: 无法获取clientId') let sseUrl = 'http://192.168.1.89:8085/monitor/account/events' try { @@ -348,16 +329,15 @@ const SSEManager = { }, handleError() { - if (!this.connection) return - try { this.connection.close() } catch {} - this.connection = null console.warn('SSE连接失败,已断开') + this.disconnect() }, disconnect() { - if (!this.connection) return - try { this.connection.close() } catch {} - this.connection = null + if (this.connection) { + try { this.connection.close() } catch {} + this.connection = null + } }, } @@ -409,8 +389,9 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) { devices.value = devices.value.filter(d => d.deviceId !== row.deviceId) deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1) - const clientId = await getClientIdFromToken() - if (row.deviceId === clientId) await logout() + if (row.deviceId === getClientIdFromToken()) { + clearLocalAuth() // 移除当前设备:只清理本地状态,不调用offline(避免覆盖removed状态) + } ElMessage.success('已移除设备') } catch (e: any) { diff --git a/electron-vue-template/src/renderer/api/auth.ts b/electron-vue-template/src/renderer/api/auth.ts index 5aea78f..61f291b 100644 --- a/electron-vue-template/src/renderer/api/auth.ts +++ b/electron-vue-template/src/renderer/api/auth.ts @@ -1,45 +1,21 @@ import { http } from './http' +export const TOKEN_KEY = 'auth_token' + export const authApi = { login(params: { username: string; password: string }) { - // 直接调用 RuoYi 后端的登录接口 return http.post('/monitor/account/login', params) }, register(params: { username: string; password: string }) { - // 直接调用 RuoYi 后端的注册接口 return http.post('/monitor/account/register', params) }, checkUsername(username: string) { - // 直接调用 RuoYi 后端的用户名检查接口 return http.get('/monitor/account/check-username', { username }) }, verifyToken(token: string) { - // 直接调用 RuoYi 后端的验证接口 return http.post('/monitor/account/verify', { token }) - }, - - logout(token: string) { - // 保留客户端的 logout(用于清理本地状态) - return http.postVoid('/api/logout', { token }) - }, - - // 以下缓存相关接口仍使用客户端服务(用于本地 SQLite 存储) - deleteTokenCache() { - return http.postVoid('/api/cache/delete?key=token') - }, - - saveToken(token: string) { - return http.postVoid('/api/cache/save', { key: 'token', value: token }) - }, - - getToken() { - return http.get('/api/cache/get?key=token') - }, - - sessionBootstrap() { - return http.get('/api/session/bootstrap') } } \ No newline at end of file diff --git a/electron-vue-template/src/renderer/utils/deviceId.ts b/electron-vue-template/src/renderer/utils/deviceId.ts index 1332ed8..1defb4f 100644 --- a/electron-vue-template/src/renderer/utils/deviceId.ts +++ b/electron-vue-template/src/renderer/utils/deviceId.ts @@ -1,14 +1,7 @@ -/** - * 设备ID管理工具 - * 从客户端服务获取硬件UUID(通过 wmic 命令) - */ - const BASE_CLIENT = 'http://localhost:8081' +const DEVICE_ID_KEY = 'device_id' -/** - * 从客户端服务获取硬件设备ID - * 客户端会使用 wmic 命令获取硬件UUID(仅Windows) - */ +// 从客户端服务获取硬件UUID(通过 wmic 命令) async function fetchDeviceIdFromClient(): Promise { const response = await fetch(`${BASE_CLIENT}/api/device-id`, { method: 'GET', @@ -30,41 +23,13 @@ async function fetchDeviceIdFromClient(): Promise { return deviceId } -/** - * 获取或创建设备ID - * 1. 优先从本地缓存读取 - * 2. 如果没有缓存,从客户端服务获取(使用硬件UUID) - * 3. 保存到本地缓存 - */ +// 获取或创建设备ID(优先读缓存,没有则从客户端服务获取硬件UUID) export async function getOrCreateDeviceId(): Promise { - try { - // 尝试从本地缓存获取 - const response = await fetch(`${BASE_CLIENT}/api/cache/get?key=deviceId`) - if (response.ok) { - const result = await response.json() - const cachedDeviceId = result?.data - if (cachedDeviceId) { - return cachedDeviceId - } - } - } catch (error) { - console.warn('从缓存读取设备ID失败:', error) - } + const cached = localStorage.getItem(DEVICE_ID_KEY) + if (cached) return cached - // 从客户端服务获取新的设备ID(硬件UUID) - const newDeviceId = await fetchDeviceIdFromClient() - - // 保存到本地缓存 - try { - await fetch(`${BASE_CLIENT}/api/cache/save`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ key: 'deviceId', value: newDeviceId }) - }) - } catch (error) { - console.warn('保存设备ID到缓存失败:', error) - } - - return newDeviceId + const deviceId = await fetchDeviceIdFromClient() + localStorage.setItem(DEVICE_ID_KEY, deviceId) + return deviceId } diff --git a/erp_client_sb/pom.xml b/erp_client_sb/pom.xml index 1fff4c9..fd3b599 100644 --- a/erp_client_sb/pom.xml +++ b/erp_client_sb/pom.xml @@ -120,6 +120,13 @@ runtime + + + com.github.oshi + oshi-core + 6.4.6 + + diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/ErrorReportAspect.java b/erp_client_sb/src/main/java/com/tashow/erp/config/ErrorReportAspect.java index 4b6e343..d261db2 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/config/ErrorReportAspect.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/ErrorReportAspect.java @@ -31,7 +31,6 @@ public class ErrorReportAspect { // 检查返回值是否表示错误 if (result instanceof JsonData) { JsonData jsonData = (JsonData) result; - // code != 0 表示失败(根据JsonData注释:0表示成功,-1表示失败,1表示处理中) if (jsonData.getCode() != null && jsonData.getCode() != 0) { // 创建一个RuntimeException来包装错误信息 String errorMsg = jsonData.getMsg() != null ? jsonData.getMsg() : "未知错误"; diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/AuthController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/AuthController.java index 8e91b5a..c05f148 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/controller/AuthController.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/AuthController.java @@ -1,16 +1,10 @@ package com.tashow.erp.controller; import com.tashow.erp.entity.AuthTokenEntity; -import com.tashow.erp.entity.CacheDataEntity; import com.tashow.erp.repository.AuthTokenRepository; -import com.tashow.erp.repository.CacheDataRepository; -import com.tashow.erp.service.IAuthService; import com.tashow.erp.utils.JsonData; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; -import java.util.Optional; /** * 客户端本地服务控制器 @@ -20,21 +14,6 @@ import java.util.Optional; public class AuthController { @Autowired private AuthTokenRepository authTokenRepository; - @Autowired - private CacheDataRepository cacheDataRepository; - - /** - * 退出登录(清理本地状态) - */ - @PostMapping("/logout") - public ResponseEntity logout(@RequestBody Map data) { - // 清理本地缓存 - try { - cacheDataRepository.deleteByCacheKey("token"); - cacheDataRepository.deleteByCacheKey("deviceId"); - } catch (Exception ignored) {} - return ResponseEntity.ok(Map.of("code", 0, "message", "退出成功")); - } /** * 保存认证密钥 @@ -51,7 +30,6 @@ public class AuthController { return JsonData.buildSuccess("认证信息保存成功"); } - @GetMapping("/auth/get") public JsonData getAuth(@RequestParam String serviceName) { return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null)); @@ -66,69 +44,6 @@ public class AuthController { return JsonData.buildSuccess("认证信息删除成功"); } - /** - * 保存缓存数据 - */ - @PostMapping("/cache/save") - public JsonData saveCache(@RequestBody Map data) { - String key = (String) data.get("key"); - String value = (String) data.get("value"); - if (key == null || value == null) return JsonData.buildError("key和value不能为空"); - CacheDataEntity entity = cacheDataRepository.findByCacheKey(key).orElse(new CacheDataEntity()); - entity.setCacheKey(key); - entity.setCacheValue(value); - cacheDataRepository.save(entity); - - return JsonData.buildSuccess("缓存数据保存成功"); - } - - /** - * 获取缓存数据 - */ - @GetMapping("/cache/get") - public JsonData getCache(@RequestParam String key) { - return JsonData.buildSuccess(cacheDataRepository.findByCacheKey(key) - .map(CacheDataEntity::getCacheValue).orElse(null)); - } - - /** - * 删除缓存数据 - */ - @DeleteMapping("/cache/remove") - public JsonData removeCache(@RequestParam String key) { - cacheDataRepository.findByCacheKey(key).ifPresent(cacheDataRepository::delete); - return JsonData.buildSuccess("缓存数据删除成功"); - } - - /** - * 删除缓存数据 - */ - @PostMapping("/cache/delete") - public JsonData deleteCacheByPost(@RequestParam String key) { - if (key == null || key.trim().isEmpty()) { - return JsonData.buildError("key不能为空"); - } - System.out.println("key: " + key); - cacheDataRepository.deleteByCacheKey(key); - return JsonData.buildSuccess("缓存数据删除成功"); - } - - /** - * 会话引导:检查SQLite中是否存在token - */ - @GetMapping("/session/bootstrap") - public JsonData sessionBootstrap() { - Optional tokenEntity = cacheDataRepository.findByCacheKey("token"); - if (tokenEntity.isEmpty()) { - return JsonData.buildError("无可用会话,请重新登录"); - } - String token = tokenEntity.get().getCacheValue(); - if (token == null || token.isEmpty()) { - return JsonData.buildError("无可用会话,请重新登录"); - } - return JsonData.buildSuccess("会话已恢复"); - } - /** * 获取设备ID */ diff --git a/erp_client_sb/src/main/java/com/tashow/erp/security/LocalJwtAuthInterceptor.java b/erp_client_sb/src/main/java/com/tashow/erp/security/LocalJwtAuthInterceptor.java index 7133e7c..6922db6 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/security/LocalJwtAuthInterceptor.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/security/LocalJwtAuthInterceptor.java @@ -27,7 +27,7 @@ public class LocalJwtAuthInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri = request.getRequestURI(); if (uri.startsWith("/libs/") || uri.startsWith("/favicon") - || uri.startsWith("/api/cache") || uri.startsWith("/api/update")) { + || uri.startsWith("/api/update")) { return true; } String auth = request.getHeader("Authorization"); diff --git a/erp_client_sb/src/main/java/com/tashow/erp/test/SeleniumWithProfile.java b/erp_client_sb/src/main/java/com/tashow/erp/test/SeleniumWithProfile.java index 5936d9c..ff85a47 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/test/SeleniumWithProfile.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/test/SeleniumWithProfile.java @@ -1,53 +1,15 @@ package com.tashow.erp.test; -import java.io.BufferedReader; -import java.io.InputStreamReader; +import com.tashow.erp.utils.DeviceUtils; + import java.util.regex.Pattern; public class SeleniumWithProfile { private static final Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:unitWeight|weight)\":(\\d+(?:\\.\\d+)?)"); public static void main(String[] args) { - String uuid = ""; - try { - Process process = Runtime.getRuntime().exec( - new String[]{"wmic", "csproduct", "get", "UUID"}); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) { - uuid = line; - break; - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } - System.out.println("电脑序列号: " + uuid); + // 使用新的 DeviceUtils 获取设备ID + String deviceId = DeviceUtils.generateDeviceId(); + System.out.println("设备ID: " + deviceId); -// ChromeDriver driver = StealthSelenium.createDriver(); -// try { -// -// // 访问目标网站 -// System.out.println("正在访问目标网站..."); -// driver.get("https://detail.1688.com/offer/600366775654.html"); -// // driver.navigate().refresh(); -// String source = driver.getPageSource(); -// String weight = ""; -// Matcher weightMatcher = WEIGHT_PATTERN.matcher(source); -// if (weightMatcher.find()) { -// String weightValue = weightMatcher.group(1); -// weight = " Weight: " + (weightValue.contains(".") ? (int) (Float.parseFloat(weightValue) * 1000) + "g" : weightValue + "g"); -// } -// System.out.println(driver.getTitle()); -// } catch (Exception e) { -// System.err.println("运行出错: " + e.getMessage()); -// e.printStackTrace(); -// }finally { -// driver.quit(); -// -// } } } diff --git a/erp_client_sb/src/main/java/com/tashow/erp/test/aa.java b/erp_client_sb/src/main/java/com/tashow/erp/test/aa.java new file mode 100644 index 0000000..658fd21 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/test/aa.java @@ -0,0 +1,15 @@ +package com.tashow.erp.test; + +import com.tashow.erp.utils.DeviceUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class aa { + @GetMapping("/a") + public String aa() { + DeviceUtils deviceUtils = new DeviceUtils(); + return deviceUtils.generateDeviceId(); + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/DeviceUtils.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/DeviceUtils.java index 40cd828..0d97419 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/utils/DeviceUtils.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/DeviceUtils.java @@ -1,33 +1,234 @@ package com.tashow.erp.utils; +import cn.hutool.crypto.SecureUtil; + import java.io.BufferedReader; import java.io.InputStreamReader; -import java.util.UUID; +import java.net.NetworkInterface; +import java.util.Enumeration; /** - * 设备工具类 + * 设备工具类 - 基于 Windows Registry + PowerShell 实现稳定的设备ID获取 + * 兼容 Windows 10/11 */ public class DeviceUtils { /** * 生成设备ID - * 优先使用硬件UUID,失败则使用随机UUID + * 多重降级策略确保始终能获取到固定的设备标识 + * + * @return 固定的设备ID,格式: 类型前缀_MD5哈希值 */ public static String generateDeviceId() { + String deviceId = null; + + // 策略1: Windows MachineGuid(注册表) + deviceId = getMachineGuid(); + if (deviceId != null) return deviceId; + + // 策略2: 硬件UUID(PowerShell) + deviceId = getHardwareUuid(); + if (deviceId != null) return deviceId; + + // 策略3: 处理器ID(PowerShell) + deviceId = getProcessorId(); + if (deviceId != null) return deviceId; + + // 策略4: 主板序列号(PowerShell) + deviceId = getMotherboardSerial(); + if (deviceId != null) return deviceId; + + // 策略5: MAC地址(Java原生) + deviceId = getMacAddress(); + if (deviceId != null) return deviceId; + + // 策略6: 系统信息组合(固定哈希) + return getSystemInfoHash(); + } + + /** + * 策略1: 获取 Windows MachineGuid(最可靠) + * 从注册表读取:HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid + * 兼容 Win10/Win11,不需要管理员权限 + */ + private static String getMachineGuid() { try { - Process process = Runtime.getRuntime().exec(new String[]{"wmic", "csproduct", "get", "UUID"}); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String command = "reg query \"HKLM\\SOFTWARE\\Microsoft\\Cryptography\" /v MachineGuid"; + Process process = Runtime.getRuntime().exec(command); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), "GBK"))) { String line; while ((line = reader.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) { - return line; + if (line.contains("MachineGuid") && line.contains("REG_SZ")) { + String[] parts = line.trim().split("\\s+"); + if (parts.length >= 3) { + String guid = parts[parts.length - 1]; + if (isValidHardwareId(guid)) { + return "MGUID_" + SecureUtil.md5(guid).substring(0, 16).toUpperCase(); + } + } + } + } + } + process.waitFor(); + } catch (Exception e) { + System.err.println("获取 MachineGuid 失败: " + e.getMessage()); + } + return null; + } + + /** + * 策略2: 获取硬件UUID(PowerShell) + * 读取 BIOS/UEFI 硬件UUID + */ + private static String getHardwareUuid() { + try { + String command = "powershell -Command \"Get-CimInstance Win32_ComputerSystemProduct | Select-Object -ExpandProperty UUID\""; + Process process = Runtime.getRuntime().exec(command); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String uuid = reader.readLine(); + if (uuid != null) { + uuid = uuid.trim(); + if (isValidHardwareId(uuid)) { + return "HW_" + SecureUtil.md5(uuid).substring(0, 16).toUpperCase(); + } + } + } + process.waitFor(); + } catch (Exception e) { + System.err.println("获取硬件 UUID 失败: " + e.getMessage()); + } + return null; + } + + /** + * 策略3: 获取处理器ID(PowerShell) + */ + private static String getProcessorId() { + try { + String command = "powershell -Command \"Get-CimInstance Win32_Processor | Select-Object -ExpandProperty ProcessorId\""; + Process process = Runtime.getRuntime().exec(command); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String processorId = reader.readLine(); + if (processorId != null) { + processorId = processorId.trim(); + if (isValidHardwareId(processorId)) { + return "CPU_" + SecureUtil.md5(processorId).substring(0, 16).toUpperCase(); + } + } + } + process.waitFor(); + } catch (Exception e) { + System.err.println("获取处理器 ID 失败: " + e.getMessage()); + } + return null; + } + + /** + * 策略4: 获取主板序列号(PowerShell) + */ + private static String getMotherboardSerial() { + try { + String command = "powershell -Command \"Get-CimInstance Win32_BaseBoard | Select-Object -ExpandProperty SerialNumber\""; + Process process = Runtime.getRuntime().exec(command); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String serial = reader.readLine(); + if (serial != null) { + serial = serial.trim(); + if (isValidHardwareId(serial)) { + return "MB_" + SecureUtil.md5(serial).substring(0, 16).toUpperCase(); + } + } + } + process.waitFor(); + } catch (Exception e) { + System.err.println("获取主板序列号失败: " + e.getMessage()); + } + return null; + } + + /** + * 策略5: 获取MAC地址(Java原生,第一个物理网卡) + */ + private static String getMacAddress() { + try { + Enumeration networks = NetworkInterface.getNetworkInterfaces(); + while (networks.hasMoreElements()) { + NetworkInterface network = networks.nextElement(); + byte[] mac = network.getHardwareAddress(); + if (mac != null && mac.length == 6) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mac.length; i++) { + sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? ":" : "")); + } + String macAddress = sb.toString(); + if (isValidMacAddress(macAddress)) { + return "MAC_" + SecureUtil.md5(macAddress).substring(0, 16).toUpperCase(); } } } } catch (Exception e) { - // 静默处理异常 + System.err.println("获取 MAC 地址失败: " + e.getMessage()); } return null; } + + /** + * 策略6: 获取系统信息组合哈希(最终降级方案) + * 基于操作系统、计算机名、用户目录等固定信息 + */ + private static String getSystemInfoHash() { + try { + StringBuilder sb = new StringBuilder(); + sb.append(System.getProperty("os.name", "")); + sb.append(System.getProperty("os.version", "")); + sb.append(System.getProperty("user.home", "")); + sb.append(System.getenv("COMPUTERNAME")); + sb.append(System.getenv("USERNAME")); + String combined = sb.toString(); + return "SYS_" + SecureUtil.md5(combined).substring(0, 16).toUpperCase(); + } catch (Exception e) { + System.err.println("获取系统信息失败: " + e.getMessage()); + // 最终的最终降级:时间戳哈希(不推荐,但保证不返回null) + return "FALLBACK_" + SecureUtil.md5(String.valueOf(System.currentTimeMillis())).substring(0, 16).toUpperCase(); + } + } + + /** + * 验证硬件ID是否有效 + */ + private static boolean isValidHardwareId(String id) { + if (id == null || id.trim().isEmpty()) { + return false; + } + String normalized = id.trim().toLowerCase(); + // 过滤无效值 + return !normalized.equals("unknown") + && !normalized.equals("n/a") + && !normalized.equals("to be filled by o.e.m.") + && !normalized.equals("default string") + && !normalized.equals("not available") + && !normalized.equals("none") + && !normalized.equals("0") + && !normalized.contains("ffffffff"); + } + + /** + * 验证MAC地址是否有效 + */ + private static boolean isValidMacAddress(String mac) { + if (mac == null || mac.trim().isEmpty()) { + return false; + } + // 过滤虚拟网卡和无效MAC + return !mac.equals("00:00:00:00:00:00") + && !mac.startsWith("00:05:69") // VMware + && !mac.startsWith("00:0C:29") // VMware + && !mac.startsWith("00:50:56") // VMware + && !mac.startsWith("00:1C:42") // Parallels + && !mac.startsWith("00:15:5D"); // Hyper-V + } } \ No newline at end of file diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index d8f8cbe..055eb63 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -106,6 +106,12 @@ 1.18.30 provided + + com.tashow.erp + erp_client_sb + 2.4.7 + compile + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java index 3c33d8e..06edefc 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java @@ -139,7 +139,26 @@ public class ClientAccountController extends BaseController { if (!"0".equals(account.getStatus())) { return AjaxResult.error("账号已被停用"); } + + // 检查设备数量限制 String clientId = loginData.get("clientId"); + if (!StringUtils.isEmpty(clientId)) { + ClientDevice currentDevice = clientDeviceMapper.selectByDeviceId(clientId); + if (currentDevice == null || "removed".equals(currentDevice.getStatus())) { + int deviceLimit = account.getDeviceLimit(); + java.util.List userDevices = clientDeviceMapper.selectByUsername(username); + int activeDeviceCount = 0; + for (ClientDevice d : userDevices) { + if (!"removed".equals(d.getStatus()) && !d.getDeviceId().equals(clientId)) { + activeDeviceCount++; + } + } + if (activeDeviceCount >= deviceLimit) { + return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备"); + } + } + } + String accessToken = Jwts.builder() .setHeaderParam("kid", jwtRsaKeyService.getKeyId()) .setSubject(username) @@ -157,6 +176,7 @@ public class ClientAccountController extends BaseController { result.put("expireTime", account.getExpireTime()); return AjaxResult.success("登录成功", result); } + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java index cdcd035..d34d855 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java @@ -43,6 +43,27 @@ public class ClientDeviceController { return account.getDeviceLimit(); } + /** + * 检查设备数量限制 + * + * @param username 用户名 + * @param currentDeviceId 当前设备ID(检查时排除此设备) + * @throws RuntimeException 如果设备数量已达上限 + */ + private void checkDeviceLimit(String username, String currentDeviceId) { + int deviceLimit = getDeviceLimit(username); + List userDevices = clientDeviceMapper.selectByUsername(username); + int activeDeviceCount = 0; + for (ClientDevice d : userDevices) { + if (!"removed".equals(d.getStatus()) && !d.getDeviceId().equals(currentDeviceId)) { + activeDeviceCount++; + } + } + if (activeDeviceCount >= deviceLimit) { + throw new RuntimeException("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备"); + } + } + /** * 查询设备配额与已使用数量 * @@ -83,20 +104,15 @@ public class ClientDeviceController { public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) { ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId()); String ip = IpUtils.getIpAddr(request); - // 从请求体读取用户名和操作系统,构建设备名称 String username = device.getUsername(); String os = device.getOs(); String deviceName = username + "@" + ip + " (" + os + ")"; if (exists == null) { // 检查设备数量限制 - int deviceLimit = getDeviceLimit(device.getUsername()); - List userDevices = clientDeviceMapper.selectByUsername(device.getUsername()); - int activeDeviceCount = 0; - for (ClientDevice d : userDevices) { - if (!"removed".equals(d.getStatus())) activeDeviceCount++; - } - if (activeDeviceCount >= deviceLimit) { - return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备"); + try { + checkDeviceLimit(device.getUsername(), device.getDeviceId()); + } catch (RuntimeException e) { + return AjaxResult.error(e.getMessage()); } device.setIp(ip); device.setStatus("online"); @@ -128,7 +144,6 @@ public class ClientDeviceController { } /** * 移除设备 - * * 根据 deviceId 删除设备绑定记录。 */ @PostMapping("/remove") @@ -173,36 +188,42 @@ public class ClientDeviceController { /** * 设备心跳 * 若设备未注册则按注册逻辑插入;已注册则更新在线状态和设备信息 + * 所有情况都检查设备数量限制,被移除设备允许重新注册(需检查配额) */ @PostMapping("/heartbeat") public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) { ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId()); String ip = IpUtils.getIpAddr(request); - // 从请求体读取用户名和操作系统,构建设备名称 - String username = device.getUsername() ; + String username = device.getUsername(); String os = device.getOs(); String deviceName = username + "@" + ip + " (" + os + ")"; + + // 统一检查设备数量限制 + try { + checkDeviceLimit(device.getUsername(), device.getDeviceId()); + } catch (RuntimeException e) { + return AjaxResult.error(e.getMessage()); + } + if (exists == null) { - // 检查设备数量限制 - int deviceLimit = getDeviceLimit(device.getUsername()); - List userDevices = clientDeviceMapper.selectByUsername(device.getUsername()); - int activeDeviceCount = 0; - for (ClientDevice d : userDevices) { - if (!"removed".equals(d.getStatus())) activeDeviceCount++; - } - if (activeDeviceCount >= deviceLimit) { - return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备"); - } + // 新设备注册 device.setIp(ip); device.setStatus("online"); device.setLastActiveAt(new java.util.Date()); device.setName(deviceName); clientDeviceMapper.insert(device); } else if ("removed".equals(exists.getStatus())) { - AjaxResult res = AjaxResult.error("设备已被移除"); - res.put("bizCode", "DEVICE_REMOVED"); - return res; + // 被移除设备重新激活 + exists.setUsername(device.getUsername()); + exists.setName(deviceName); + exists.setOs(device.getOs()); + exists.setStatus("online"); + exists.setIp(ip); + exists.setLocation(device.getLocation()); + exists.setLastActiveAt(new java.util.Date()); + clientDeviceMapper.updateByDeviceId(exists); } else { + // 已存在设备更新 exists.setUsername(device.getUsername()); exists.setStatus("online"); exists.setIp(ip); diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 476dbee..6e52be2 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -87,19 +87,20 @@ spring: # 密码 # password: password: 123123 - - # 连接超时时间 - timeout: 10s + # 连接超时时间(降低超时,快速失败) + timeout: 3s lettuce: pool: - # 连接池中的最小空闲连接 - min-idle: 0 + # 连接池中的最小空闲连接(保持预热连接,避免临时建连) + min-idle: 2 # 连接池中的最大空闲连接 - max-idle: 8 - # 连接池的最大数据库连接数 - max-active: 8 - # #连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms + max-idle: 10 + # 连接池的最大数据库连接数(增加以应对并发) + max-active: 20 + # 连接池最大阻塞等待时间(设置合理超时,避免无限等待) + max-wait: 3000ms + # 关闭超时时间 + shutdown-timeout: 100ms # token配置 token: diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development index 5882300..95510a9 100644 --- a/ruoyi-ui/.env.development +++ b/ruoyi-ui/.env.development @@ -5,7 +5,7 @@ VUE_APP_TITLE =ERP管理系统 ENV = 'development' # ERP管理系统/开发环境 -VUE_APP_BASE_API = 'http://localhost:8080' +VUE_APP_BASE_API = 'http://192.168.1.89:8085' # 路由懒加载 VUE_CLI_BABEL_TRANSPILE_MODULES = true