feat(client): 实现稳定的设备ID生成与认证优化
- 重构设备ID生成逻辑,采用多重降级策略确保唯一性与稳定性- 移除客户端SQLite缓存依赖,改用localStorage存储token与设备ID - 优化认证流程,简化token管理与会话恢复逻辑- 增加设备数量限制检查,防止超出配额 - 更新SSE连接逻辑,适配新的认证机制 - 调整Redis连接池配置,提升并发性能与稳定性 - 移除冗余的缓存接口与本地退出逻辑 - 修复设备移除时的状态处理问题,避免重复调用offline接口 - 引入OSHI库用于硬件信息采集(备用方案)- 更新开发环境API地址配置
This commit is contained in:
@@ -5,7 +5,6 @@
|
|||||||
"main": "main/main.js",
|
"main": "main/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/dev-server.js",
|
"dev": "node scripts/dev-server.js",
|
||||||
"build": "node scripts/build.js && electron-builder",
|
|
||||||
"build:win": "node scripts/build.js && electron-builder --win",
|
"build:win": "node scripts/build.js && electron-builder --win",
|
||||||
"build:mac": "node scripts/build.js && electron-builder --mac",
|
"build:mac": "node scripts/build.js && electron-builder --mac",
|
||||||
"build:linux": "node scripts/build.js && electron-builder --linux"
|
"build:linux": "node scripts/build.js && electron-builder --linux"
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ function openAppIfNotOpened() {
|
|||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安全关闭启动画面
|
// 安全关闭启动画面
|
||||||
if (splashWindow && !splashWindow.isDestroyed()) {
|
if (splashWindow && !splashWindow.isDestroyed()) {
|
||||||
splashWindow.close();
|
splashWindow.close();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmoun
|
|||||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import 'element-plus/dist/index.css'
|
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 {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
||||||
import {getOrCreateDeviceId} from './utils/deviceId'
|
import {getOrCreateDeviceId} from './utils/deviceId'
|
||||||
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
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 }) {
|
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(TOKEN_KEY, data.token)
|
||||||
isAuthenticated.value = true
|
isAuthenticated.value = true
|
||||||
showAuthDialog.value = false
|
showAuthDialog.value = false
|
||||||
showRegDialog.value = false
|
showRegDialog.value = false
|
||||||
|
|
||||||
try {
|
currentUsername.value = getUsernameFromToken(data.token)
|
||||||
await authApi.saveToken(data.token)
|
userPermissions.value = data.permissions || ''
|
||||||
const username = getUsernameFromToken(data.token)
|
vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null
|
||||||
currentUsername.value = username
|
|
||||||
userPermissions.value = data?.permissions || ''
|
|
||||||
vipExpireTime.value = data?.expireTime ? new Date(data.expireTime) : null
|
|
||||||
|
|
||||||
// 获取设备ID并注册设备
|
|
||||||
const deviceId = await getOrCreateDeviceId()
|
const deviceId = await getOrCreateDeviceId()
|
||||||
await deviceApi.register({
|
await deviceApi.register({
|
||||||
username,
|
username: currentUsername.value,
|
||||||
deviceId,
|
deviceId,
|
||||||
os: navigator.platform
|
os: navigator.platform
|
||||||
})
|
})
|
||||||
@@ -168,26 +166,13 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
await authApi.deleteTokenCache()
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
ElMessage.error(e?.message || '设备注册失败')
|
ElMessage.error(e?.message || '设备注册失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
function clearLocalAuth() {
|
||||||
try {
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
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()
|
|
||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
currentUsername.value = ''
|
currentUsername.value = ''
|
||||||
userPermissions.value = ''
|
userPermissions.value = ''
|
||||||
@@ -197,6 +182,16 @@ async function logout() {
|
|||||||
SSEManager.disconnect()
|
SSEManager.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
const deviceId = getClientIdFromToken()
|
||||||
|
if (deviceId) await deviceApi.offline({ deviceId })
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('离线通知失败:', error)
|
||||||
|
}
|
||||||
|
clearLocalAuth()
|
||||||
|
}
|
||||||
|
|
||||||
async function handleUserClick() {
|
async function handleUserClick() {
|
||||||
if (!isAuthenticated.value) {
|
if (!isAuthenticated.value) {
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
@@ -220,47 +215,42 @@ function showRegisterDialog() {
|
|||||||
|
|
||||||
function backToLogin() {
|
function backToLogin() {
|
||||||
showRegDialog.value = false
|
showRegDialog.value = false
|
||||||
|
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
try {
|
try {
|
||||||
await authApi.sessionBootstrap().catch(() => undefined)
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
const tokenRes: any = await authApi.getToken()
|
if (!token) {
|
||||||
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
||||||
|
showAuthDialog.value = true
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SSEManager.connect()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
await authApi.deleteTokenCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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)) {
|
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getClientIdFromToken(token?: string) {
|
|
||||||
try {
|
|
||||||
let t = token
|
|
||||||
if (!t) {
|
|
||||||
const tokenRes: any = await authApi.getToken()
|
|
||||||
t = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getClientIdFromToken(token?: string) {
|
||||||
|
try {
|
||||||
|
const t = token || localStorage.getItem(TOKEN_KEY)
|
||||||
if (!t) return ''
|
if (!t) return ''
|
||||||
const payload = JSON.parse(atob(t.split('.')[1]))
|
return JSON.parse(atob(t.split('.')[1])).clientId || ''
|
||||||
return payload.clientId || ''
|
|
||||||
} catch {
|
} catch {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -268,32 +258,23 @@ async function getClientIdFromToken(token?: string) {
|
|||||||
|
|
||||||
function getUsernameFromToken(token: string) {
|
function getUsernameFromToken(token: string) {
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
return JSON.parse(atob(token.split('.')[1])).username || ''
|
||||||
return payload.username || ''
|
|
||||||
} catch {
|
} catch {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSE管理器
|
|
||||||
const SSEManager = {
|
const SSEManager = {
|
||||||
connection: null as EventSource | null,
|
connection: null as EventSource | null,
|
||||||
async connect() {
|
async connect() {
|
||||||
if (this.connection) return
|
if (this.connection) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenRes: any = await authApi.getToken()
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
if (!token) return console.warn('SSE连接失败: 没有token')
|
||||||
if (!token) {
|
|
||||||
console.warn('SSE连接失败: 没有有效的 token')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientId = await getClientIdFromToken(token)
|
const clientId = getClientIdFromToken(token)
|
||||||
if (!clientId) {
|
if (!clientId) return console.warn('SSE连接失败: 无法获取clientId')
|
||||||
console.warn('SSE连接失败: 无法从 token 获取 clientId')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let sseUrl = 'http://192.168.1.89:8085/monitor/account/events'
|
let sseUrl = 'http://192.168.1.89:8085/monitor/account/events'
|
||||||
try {
|
try {
|
||||||
@@ -348,16 +329,15 @@ const SSEManager = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleError() {
|
handleError() {
|
||||||
if (!this.connection) return
|
|
||||||
try { this.connection.close() } catch {}
|
|
||||||
this.connection = null
|
|
||||||
console.warn('SSE连接失败,已断开')
|
console.warn('SSE连接失败,已断开')
|
||||||
|
this.disconnect()
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (!this.connection) return
|
if (this.connection) {
|
||||||
try { this.connection.close() } catch {}
|
try { this.connection.close() } catch {}
|
||||||
this.connection = null
|
this.connection = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,8 +389,9 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
|||||||
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
||||||
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
|
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
|
||||||
|
|
||||||
const clientId = await getClientIdFromToken()
|
if (row.deviceId === getClientIdFromToken()) {
|
||||||
if (row.deviceId === clientId) await logout()
|
clearLocalAuth() // 移除当前设备:只清理本地状态,不调用offline(避免覆盖removed状态)
|
||||||
|
}
|
||||||
|
|
||||||
ElMessage.success('已移除设备')
|
ElMessage.success('已移除设备')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -1,45 +1,21 @@
|
|||||||
import { http } from './http'
|
import { http } from './http'
|
||||||
|
|
||||||
|
export const TOKEN_KEY = 'auth_token'
|
||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
login(params: { username: string; password: string }) {
|
login(params: { username: string; password: string }) {
|
||||||
// 直接调用 RuoYi 后端的登录接口
|
|
||||||
return http.post('/monitor/account/login', params)
|
return http.post('/monitor/account/login', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
register(params: { username: string; password: string }) {
|
register(params: { username: string; password: string }) {
|
||||||
// 直接调用 RuoYi 后端的注册接口
|
|
||||||
return http.post('/monitor/account/register', params)
|
return http.post('/monitor/account/register', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
checkUsername(username: string) {
|
checkUsername(username: string) {
|
||||||
// 直接调用 RuoYi 后端的用户名检查接口
|
|
||||||
return http.get('/monitor/account/check-username', { username })
|
return http.get('/monitor/account/check-username', { username })
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyToken(token: string) {
|
verifyToken(token: string) {
|
||||||
// 直接调用 RuoYi 后端的验证接口
|
|
||||||
return http.post('/monitor/account/verify', { token })
|
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')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
/**
|
|
||||||
* 设备ID管理工具
|
|
||||||
* 从客户端服务获取硬件UUID(通过 wmic 命令)
|
|
||||||
*/
|
|
||||||
|
|
||||||
const BASE_CLIENT = 'http://localhost:8081'
|
const BASE_CLIENT = 'http://localhost:8081'
|
||||||
|
const DEVICE_ID_KEY = 'device_id'
|
||||||
|
|
||||||
/**
|
// 从客户端服务获取硬件UUID(通过 wmic 命令)
|
||||||
* 从客户端服务获取硬件设备ID
|
|
||||||
* 客户端会使用 wmic 命令获取硬件UUID(仅Windows)
|
|
||||||
*/
|
|
||||||
async function fetchDeviceIdFromClient(): Promise<string> {
|
async function fetchDeviceIdFromClient(): Promise<string> {
|
||||||
const response = await fetch(`${BASE_CLIENT}/api/device-id`, {
|
const response = await fetch(`${BASE_CLIENT}/api/device-id`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -30,41 +23,13 @@ async function fetchDeviceIdFromClient(): Promise<string> {
|
|||||||
return deviceId
|
return deviceId
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 获取或创建设备ID(优先读缓存,没有则从客户端服务获取硬件UUID)
|
||||||
* 获取或创建设备ID
|
|
||||||
* 1. 优先从本地缓存读取
|
|
||||||
* 2. 如果没有缓存,从客户端服务获取(使用硬件UUID)
|
|
||||||
* 3. 保存到本地缓存
|
|
||||||
*/
|
|
||||||
export async function getOrCreateDeviceId(): Promise<string> {
|
export async function getOrCreateDeviceId(): Promise<string> {
|
||||||
try {
|
const cached = localStorage.getItem(DEVICE_ID_KEY)
|
||||||
// 尝试从本地缓存获取
|
if (cached) return cached
|
||||||
const response = await fetch(`${BASE_CLIENT}/api/cache/get?key=deviceId`)
|
|
||||||
if (response.ok) {
|
const deviceId = await fetchDeviceIdFromClient()
|
||||||
const result = await response.json()
|
localStorage.setItem(DEVICE_ID_KEY, deviceId)
|
||||||
const cachedDeviceId = result?.data
|
return deviceId
|
||||||
if (cachedDeviceId) {
|
|
||||||
return cachedDeviceId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('从缓存读取设备ID失败:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从客户端服务获取新的设备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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,13 @@
|
|||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OSHI for hardware information -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.oshi</groupId>
|
||||||
|
<artifactId>oshi-core</artifactId>
|
||||||
|
<version>6.4.6</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ public class ErrorReportAspect {
|
|||||||
// 检查返回值是否表示错误
|
// 检查返回值是否表示错误
|
||||||
if (result instanceof JsonData) {
|
if (result instanceof JsonData) {
|
||||||
JsonData jsonData = (JsonData) result;
|
JsonData jsonData = (JsonData) result;
|
||||||
// code != 0 表示失败(根据JsonData注释:0表示成功,-1表示失败,1表示处理中)
|
|
||||||
if (jsonData.getCode() != null && jsonData.getCode() != 0) {
|
if (jsonData.getCode() != null && jsonData.getCode() != 0) {
|
||||||
// 创建一个RuntimeException来包装错误信息
|
// 创建一个RuntimeException来包装错误信息
|
||||||
String errorMsg = jsonData.getMsg() != null ? jsonData.getMsg() : "未知错误";
|
String errorMsg = jsonData.getMsg() != null ? jsonData.getMsg() : "未知错误";
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
package com.tashow.erp.controller;
|
package com.tashow.erp.controller;
|
||||||
import com.tashow.erp.entity.AuthTokenEntity;
|
import com.tashow.erp.entity.AuthTokenEntity;
|
||||||
import com.tashow.erp.entity.CacheDataEntity;
|
|
||||||
import com.tashow.erp.repository.AuthTokenRepository;
|
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 com.tashow.erp.utils.JsonData;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 客户端本地服务控制器
|
* 客户端本地服务控制器
|
||||||
@@ -20,21 +14,6 @@ import java.util.Optional;
|
|||||||
public class AuthController {
|
public class AuthController {
|
||||||
@Autowired
|
@Autowired
|
||||||
private AuthTokenRepository authTokenRepository;
|
private AuthTokenRepository authTokenRepository;
|
||||||
@Autowired
|
|
||||||
private CacheDataRepository cacheDataRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退出登录(清理本地状态)
|
|
||||||
*/
|
|
||||||
@PostMapping("/logout")
|
|
||||||
public ResponseEntity<?> logout(@RequestBody Map<String, Object> 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("认证信息保存成功");
|
return JsonData.buildSuccess("认证信息保存成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/auth/get")
|
@GetMapping("/auth/get")
|
||||||
public JsonData getAuth(@RequestParam String serviceName) {
|
public JsonData getAuth(@RequestParam String serviceName) {
|
||||||
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null));
|
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null));
|
||||||
@@ -66,69 +44,6 @@ public class AuthController {
|
|||||||
return JsonData.buildSuccess("认证信息删除成功");
|
return JsonData.buildSuccess("认证信息删除成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存缓存数据
|
|
||||||
*/
|
|
||||||
@PostMapping("/cache/save")
|
|
||||||
public JsonData saveCache(@RequestBody Map<String, Object> 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<CacheDataEntity> 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
|
* 获取设备ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class LocalJwtAuthInterceptor implements HandlerInterceptor {
|
|||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
if (uri.startsWith("/libs/") || uri.startsWith("/favicon")
|
if (uri.startsWith("/libs/") || uri.startsWith("/favicon")
|
||||||
|| uri.startsWith("/api/cache") || uri.startsWith("/api/update")) {
|
|| uri.startsWith("/api/update")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
String auth = request.getHeader("Authorization");
|
String auth = request.getHeader("Authorization");
|
||||||
|
|||||||
@@ -1,53 +1,15 @@
|
|||||||
package com.tashow.erp.test;
|
package com.tashow.erp.test;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import com.tashow.erp.utils.DeviceUtils;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class SeleniumWithProfile {
|
public class SeleniumWithProfile {
|
||||||
private static final Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:unitWeight|weight)\":(\\d+(?:\\.\\d+)?)");
|
private static final Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:unitWeight|weight)\":(\\d+(?:\\.\\d+)?)");
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
String uuid = "";
|
// 使用新的 DeviceUtils 获取设备ID
|
||||||
try {
|
String deviceId = DeviceUtils.generateDeviceId();
|
||||||
Process process = Runtime.getRuntime().exec(
|
System.out.println("设备ID: " + deviceId);
|
||||||
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);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
erp_client_sb/src/main/java/com/tashow/erp/test/aa.java
Normal file
15
erp_client_sb/src/main/java/com/tashow/erp/test/aa.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,234 @@
|
|||||||
package com.tashow.erp.utils;
|
package com.tashow.erp.utils;
|
||||||
|
|
||||||
|
import cn.hutool.crypto.SecureUtil;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
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 {
|
public class DeviceUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成设备ID
|
* 生成设备ID
|
||||||
* 优先使用硬件UUID,失败则使用随机UUID
|
* 多重降级策略确保始终能获取到固定的设备标识
|
||||||
|
*
|
||||||
|
* @return 固定的设备ID,格式: 类型前缀_MD5哈希值
|
||||||
*/
|
*/
|
||||||
public static String generateDeviceId() {
|
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 {
|
try {
|
||||||
Process process = Runtime.getRuntime().exec(new String[]{"wmic", "csproduct", "get", "UUID"});
|
String command = "reg query \"HKLM\\SOFTWARE\\Microsoft\\Cryptography\" /v MachineGuid";
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
Process process = Runtime.getRuntime().exec(command);
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(process.getInputStream(), "GBK"))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
line = line.trim();
|
if (line.contains("MachineGuid") && line.contains("REG_SZ")) {
|
||||||
if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) {
|
String[] parts = line.trim().split("\\s+");
|
||||||
return line;
|
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<NetworkInterface> 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) {
|
} catch (Exception e) {
|
||||||
// 静默处理异常
|
System.err.println("获取 MAC 地址失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
return null;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +106,12 @@
|
|||||||
<version>1.18.30</version>
|
<version>1.18.30</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.tashow.erp</groupId>
|
||||||
|
<artifactId>erp_client_sb</artifactId>
|
||||||
|
<version>2.4.7</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,26 @@ public class ClientAccountController extends BaseController {
|
|||||||
if (!"0".equals(account.getStatus())) {
|
if (!"0".equals(account.getStatus())) {
|
||||||
return AjaxResult.error("账号已被停用");
|
return AjaxResult.error("账号已被停用");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查设备数量限制
|
||||||
String clientId = loginData.get("clientId");
|
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<ClientDevice> 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()
|
String accessToken = Jwts.builder()
|
||||||
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
.setSubject(username)
|
.setSubject(username)
|
||||||
@@ -160,6 +179,7 @@ public class ClientAccountController extends BaseController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证token
|
* 验证token
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,6 +43,27 @@ public class ClientDeviceController {
|
|||||||
return account.getDeviceLimit();
|
return account.getDeviceLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查设备数量限制
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param currentDeviceId 当前设备ID(检查时排除此设备)
|
||||||
|
* @throws RuntimeException 如果设备数量已达上限
|
||||||
|
*/
|
||||||
|
private void checkDeviceLimit(String username, String currentDeviceId) {
|
||||||
|
int deviceLimit = getDeviceLimit(username);
|
||||||
|
List<ClientDevice> 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) {
|
public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) {
|
||||||
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
||||||
String ip = IpUtils.getIpAddr(request);
|
String ip = IpUtils.getIpAddr(request);
|
||||||
// 从请求体读取用户名和操作系统,构建设备名称
|
|
||||||
String username = device.getUsername();
|
String username = device.getUsername();
|
||||||
String os = device.getOs();
|
String os = device.getOs();
|
||||||
String deviceName = username + "@" + ip + " (" + os + ")";
|
String deviceName = username + "@" + ip + " (" + os + ")";
|
||||||
if (exists == null) {
|
if (exists == null) {
|
||||||
// 检查设备数量限制
|
// 检查设备数量限制
|
||||||
int deviceLimit = getDeviceLimit(device.getUsername());
|
try {
|
||||||
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(device.getUsername());
|
checkDeviceLimit(device.getUsername(), device.getDeviceId());
|
||||||
int activeDeviceCount = 0;
|
} catch (RuntimeException e) {
|
||||||
for (ClientDevice d : userDevices) {
|
return AjaxResult.error(e.getMessage());
|
||||||
if (!"removed".equals(d.getStatus())) activeDeviceCount++;
|
|
||||||
}
|
|
||||||
if (activeDeviceCount >= deviceLimit) {
|
|
||||||
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
|
|
||||||
}
|
}
|
||||||
device.setIp(ip);
|
device.setIp(ip);
|
||||||
device.setStatus("online");
|
device.setStatus("online");
|
||||||
@@ -128,7 +144,6 @@ public class ClientDeviceController {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 移除设备
|
* 移除设备
|
||||||
*
|
|
||||||
* 根据 deviceId 删除设备绑定记录。
|
* 根据 deviceId 删除设备绑定记录。
|
||||||
*/
|
*/
|
||||||
@PostMapping("/remove")
|
@PostMapping("/remove")
|
||||||
@@ -173,36 +188,42 @@ public class ClientDeviceController {
|
|||||||
/**
|
/**
|
||||||
* 设备心跳
|
* 设备心跳
|
||||||
* 若设备未注册则按注册逻辑插入;已注册则更新在线状态和设备信息
|
* 若设备未注册则按注册逻辑插入;已注册则更新在线状态和设备信息
|
||||||
|
* 所有情况都检查设备数量限制,被移除设备允许重新注册(需检查配额)
|
||||||
*/
|
*/
|
||||||
@PostMapping("/heartbeat")
|
@PostMapping("/heartbeat")
|
||||||
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
|
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
|
||||||
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
||||||
String ip = IpUtils.getIpAddr(request);
|
String ip = IpUtils.getIpAddr(request);
|
||||||
// 从请求体读取用户名和操作系统,构建设备名称
|
|
||||||
String username = device.getUsername();
|
String username = device.getUsername();
|
||||||
String os = device.getOs();
|
String os = device.getOs();
|
||||||
String deviceName = username + "@" + ip + " (" + os + ")";
|
String deviceName = username + "@" + ip + " (" + os + ")";
|
||||||
|
|
||||||
|
// 统一检查设备数量限制
|
||||||
|
try {
|
||||||
|
checkDeviceLimit(device.getUsername(), device.getDeviceId());
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
if (exists == null) {
|
if (exists == null) {
|
||||||
// 检查设备数量限制
|
// 新设备注册
|
||||||
int deviceLimit = getDeviceLimit(device.getUsername());
|
|
||||||
List<ClientDevice> 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.setIp(ip);
|
||||||
device.setStatus("online");
|
device.setStatus("online");
|
||||||
device.setLastActiveAt(new java.util.Date());
|
device.setLastActiveAt(new java.util.Date());
|
||||||
device.setName(deviceName);
|
device.setName(deviceName);
|
||||||
clientDeviceMapper.insert(device);
|
clientDeviceMapper.insert(device);
|
||||||
} else if ("removed".equals(exists.getStatus())) {
|
} else if ("removed".equals(exists.getStatus())) {
|
||||||
AjaxResult res = AjaxResult.error("设备已被移除");
|
// 被移除设备重新激活
|
||||||
res.put("bizCode", "DEVICE_REMOVED");
|
exists.setUsername(device.getUsername());
|
||||||
return res;
|
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 {
|
} else {
|
||||||
|
// 已存在设备更新
|
||||||
exists.setUsername(device.getUsername());
|
exists.setUsername(device.getUsername());
|
||||||
exists.setStatus("online");
|
exists.setStatus("online");
|
||||||
exists.setIp(ip);
|
exists.setIp(ip);
|
||||||
|
|||||||
@@ -87,19 +87,20 @@ spring:
|
|||||||
# 密码
|
# 密码
|
||||||
# password:
|
# password:
|
||||||
password: 123123
|
password: 123123
|
||||||
|
# 连接超时时间(降低超时,快速失败)
|
||||||
# 连接超时时间
|
timeout: 3s
|
||||||
timeout: 10s
|
|
||||||
lettuce:
|
lettuce:
|
||||||
pool:
|
pool:
|
||||||
# 连接池中的最小空闲连接
|
# 连接池中的最小空闲连接(保持预热连接,避免临时建连)
|
||||||
min-idle: 0
|
min-idle: 2
|
||||||
# 连接池中的最大空闲连接
|
# 连接池中的最大空闲连接
|
||||||
max-idle: 8
|
max-idle: 10
|
||||||
# 连接池的最大数据库连接数
|
# 连接池的最大数据库连接数(增加以应对并发)
|
||||||
max-active: 8
|
max-active: 20
|
||||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
# 连接池最大阻塞等待时间(设置合理超时,避免无限等待)
|
||||||
max-wait: -1ms
|
max-wait: 3000ms
|
||||||
|
# 关闭超时时间
|
||||||
|
shutdown-timeout: 100ms
|
||||||
|
|
||||||
# token配置
|
# token配置
|
||||||
token:
|
token:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ VUE_APP_TITLE =ERP管理系统
|
|||||||
ENV = 'development'
|
ENV = 'development'
|
||||||
|
|
||||||
# ERP管理系统/开发环境
|
# ERP管理系统/开发环境
|
||||||
VUE_APP_BASE_API = 'http://localhost:8080'
|
VUE_APP_BASE_API = 'http://192.168.1.89:8085'
|
||||||
|
|
||||||
# 路由懒加载
|
# 路由懒加载
|
||||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||||
|
|||||||
Reference in New Issue
Block a user