feat(device): 更新设备管理功能并优化错误处理- 修改设备更新接口路径从 /updateExpire 到 /update- 添加设备注册时获取计算机名称功能

- 优化设备配额检查逻辑,增加账号存在性验证- 更新前端设备列表刷新逻辑,使用保存的用户名参数
- 修改账号编辑表单,禁用已存在账号的用户名和账号名编辑
-优化跟卖精灵打开功能的错误提示和异常处理- 添加页面刷新 IPC通信功能
- 限制用户名输入只能包含字母、数字和下划线
- 移除冗余的本地 IP 获取函数- 升级 erp_client_sb 模块版本至 2.4.9
This commit is contained in:
2025-10-22 14:18:28 +08:00
parent 17b6a7b9f9
commit 5468dc53fc
15 changed files with 115 additions and 96 deletions

View File

@@ -804,6 +804,9 @@ ipcMain.handle('set-launch-config', (event, launchConfig: { autoLaunch: boolean;
return { success: true }; return { success: true };
}); });
// 刷新页面
ipcMain.handle('reload', () => mainWindow?.webContents.reload());
async function getFileSize(url: string): Promise<number> { async function getFileSize(url: string): Promise<number> {
return new Promise((resolve) => { return new Promise((resolve) => {

View File

@@ -34,6 +34,9 @@ const electronAPI = {
getLaunchConfig: () => ipcRenderer.invoke('get-launch-config'), getLaunchConfig: () => ipcRenderer.invoke('get-launch-config'),
setLaunchConfig: (config: { autoLaunch: boolean; launchMinimized: boolean }) => ipcRenderer.invoke('set-launch-config', config), setLaunchConfig: (config: { autoLaunch: boolean; launchMinimized: boolean }) => ipcRenderer.invoke('set-launch-config', config),
// 刷新页面 API
reload: () => ipcRenderer.invoke('reload'),
onDownloadProgress: (callback: (progress: any) => void) => { onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.removeAllListeners('download-progress') ipcRenderer.removeAllListeners('download-progress')
ipcRenderer.on('download-progress', (event, progress) => callback(progress)) ipcRenderer.on('download-progress', (event, progress) => callback(progress))

View File

@@ -150,7 +150,7 @@ function goForward() {
} }
function reloadPage() { function reloadPage() {
window.location.reload() window.electronAPI.reload()
} }
function handleMenuSelect(key: string) { function handleMenuSelect(key: string) {

View File

@@ -14,18 +14,6 @@ export interface DeviceQuota {
used: number used: number
} }
/**
* 获取本机内网IP地址
*/
async function getLocalIP(): Promise<string> {
try {
const res = await http.get<{ data: string }>('/api/system/local-ip')
return res.data
} catch {
return '127.0.0.1'
}
}
export const deviceApi = { export const deviceApi = {
getQuota(username: string) { getQuota(username: string) {
return http.get<{ data: DeviceQuota }>('/monitor/device/quota', { username }) return http.get<{ data: DeviceQuota }>('/monitor/device/quota', { username })
@@ -36,8 +24,15 @@ export const deviceApi = {
}, },
async register(payload: { username: string; deviceId: string; os?: string }) { async register(payload: { username: string; deviceId: string; os?: string }) {
const ip = await getLocalIP() const [ipRes, nameRes] = await Promise.all([
return http.post('/monitor/device/register', { ...payload, ip }) http.get<{ data: string }>('/api/system/local-ip'),
http.get<{ data: string }>('/api/system/computer-name')
])
return http.post('/monitor/device/register', {
...payload,
ip: ipRes.data,
computerName: nameRes.data
})
}, },
remove(payload: { deviceId: string; username: string }) { remove(payload: { deviceId: string; username: string }) {

View File

@@ -1,5 +1,4 @@
export type HttpMethod = 'GET' | 'POST' | 'DELETE'; export type HttpMethod = 'GET' | 'POST' | 'DELETE';
export const CONFIG = { export const CONFIG = {
CLIENT_BASE: 'http://localhost:8081', CLIENT_BASE: 'http://localhost:8081',
RUOYI_BASE: 'http://8.138.23.49:8085', RUOYI_BASE: 'http://8.138.23.49:8085',

View File

@@ -259,8 +259,10 @@ async function openGenmaiSpirit() {
genmaiLoading.value = true genmaiLoading.value = true
try { try {
await systemApi.openGenmaiSpirit() await systemApi.openGenmaiSpirit()
showMessage('跟卖精灵已打开', 'success')
} catch (error: any) { } catch (error: any) {
showMessage(error.message || '打开跟卖精灵失败', 'error') const errorMsg = error?.msg || error?.message || '打开跟卖精灵失败'
showMessage(errorMsg, 'error')
} finally { } finally {
genmaiLoading.value = false genmaiLoading.value = false
} }

View File

@@ -35,6 +35,10 @@ const canRegister = computed(() => {
usernameCheckResult.value === true usernameCheckResult.value === true
}) })
function filterUsername(value: string) {
registerForm.value.username = value.replace(/[^a-zA-Z0-9_]/g, '')
}
async function checkUsernameAvailability() { async function checkUsernameAvailability() {
if (!registerForm.value.username) { if (!registerForm.value.username) {
usernameCheckResult.value = null usernameCheckResult.value = null
@@ -123,10 +127,11 @@ function backToLogin() {
<el-input <el-input
v-model="registerForm.username" v-model="registerForm.username"
placeholder="请输入用户名" placeholder="请输入用户名(字母、数字、下划线)"
size="large" size="large"
style="margin-bottom: 15px;" style="margin-bottom: 15px;"
:disabled="registerLoading" :disabled="registerLoading"
@input="filterUsername"
@blur="checkUsernameAvailability"> @blur="checkUsernameAvailability">
</el-input> </el-input>

View File

@@ -10,7 +10,7 @@
</parent> </parent>
<groupId>com.tashow.erp</groupId> <groupId>com.tashow.erp</groupId>
<artifactId>erp_client_sb</artifactId> <artifactId>erp_client_sb</artifactId>
<version>2.4.8</version> <version>2.4.9</version>
<name>erp_client_sb</name> <name>erp_client_sb</name>
<description>erp客户端</description> <description>erp客户端</description>
<properties> <properties>

View File

@@ -65,12 +65,13 @@ public class SystemController {
} }
@GetMapping("/local-ip") @GetMapping("/local-ip")
public JsonData getLocalIp() { public JsonData getLocalIp() throws Exception {
try {
return JsonData.buildSuccess(java.net.InetAddress.getLocalHost().getHostAddress()); return JsonData.buildSuccess(java.net.InetAddress.getLocalHost().getHostAddress());
} catch (Exception e) {
return JsonData.buildSuccess("127.0.0.1");
} }
@GetMapping("/computer-name")
public JsonData getComputerName() {
return JsonData.buildSuccess(System.getenv("COMPUTERNAME"));
} }
@GetMapping("/version") @GetMapping("/version")
@@ -84,8 +85,14 @@ public class SystemController {
} }
@PostMapping("/genmai/open") @PostMapping("/genmai/open")
public void openGenmaiWebsite() { public JsonData openGenmaiWebsite() {
try {
genmaiService.openGenmaiWebsite(); genmaiService.openGenmaiWebsite();
return JsonData.buildSuccess("跟卖精灵已打开");
} catch (Exception e) {
logger.error("打开跟卖精灵失败", e);
return JsonData.buildError(e.getMessage() != null ? e.getMessage() : "打开跟卖精灵失败");
}
} }
@GetMapping("/proxy/image") @GetMapping("/proxy/image")

View File

@@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qiniu.util.UrlUtils; import com.qiniu.util.UrlUtils;
import io.github.bonigarcia.wdm.WebDriverManager; import io.github.bonigarcia.wdm.WebDriverManager;
import lombok.SneakyThrows;
import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeDriver;
@@ -25,10 +24,12 @@ public class GenmaiServiceImpl {
private String updateGenmaijlToken; private String updateGenmaijlToken;
private final RestTemplate restTemplate = new RestTemplate(); private final RestTemplate restTemplate = new RestTemplate();
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
@SneakyThrows
public void openGenmaiWebsite() { public void openGenmaiWebsite() {
try {
// 先关闭所有Chrome进程 // 先关闭所有Chrome进程
Runtime.getRuntime().exec("taskkill /f /im chrome.exe"); Runtime.getRuntime().exec("taskkill /f /im chrome.exe");
Thread.sleep(1000); // 等待进程关闭
WebDriverManager.chromedriver().setup(); WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions(); ChromeOptions options = new ChromeOptions();
String username = System.getProperty("user.name", "user"); String username = System.getProperty("user.name", "user");
@@ -36,15 +37,19 @@ public class GenmaiServiceImpl {
char firstChar = username.charAt(0); char firstChar = username.charAt(0);
char flippedFirstChar = Character.isUpperCase(firstChar) ? Character.toLowerCase(firstChar) : Character.toUpperCase(firstChar); char flippedFirstChar = Character.isUpperCase(firstChar) ? Character.toLowerCase(firstChar) : Character.toUpperCase(firstChar);
safeUsername = flippedFirstChar + username.substring(1); safeUsername = flippedFirstChar + username.substring(1);
String chromeUserData = System.getProperty("user.home").replace(username, UrlUtils.urlEncode(safeUsername) ) String chromeUserData = System.getProperty("user.home").replace(username, UrlUtils.urlEncode(safeUsername))
+ "\\AppData\\Local\\Google\\Chrome\\User Data"; + "\\AppData\\Local\\Google\\Chrome\\User Data";
options.addArguments("user-data-dir=" + chromeUserData.toLowerCase()); options.addArguments("user-data-dir=" + chromeUserData.toLowerCase());
options.addArguments("profile-directory=Default"); options.addArguments("profile-directory=Default");
WebDriver driver = new ChromeDriver(options); WebDriver driver = new ChromeDriver(options);
driver.get("https://www.genmaijl.com/#/profile"); driver.get("https://www.genmaijl.com/#/profile");
JavascriptExecutor js = (JavascriptExecutor) driver; JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript(String.format("localStorage.setItem('token','%s')", checkTokenExpired())); js.executeScript(String.format("localStorage.setItem('token','%s')", checkTokenExpired()));
driver.navigate().refresh(); driver.navigate().refresh();
} catch (Exception e) {
throw new RuntimeException("打开跟卖精灵失败请确保已安装Chrome浏览器", e);
}
} }
/** /**
* 获取token验证token是否过期 * 获取token验证token是否过期
@@ -67,8 +72,8 @@ public class GenmaiServiceImpl {
/** /**
* 登录 * 登录
*/ */
@SneakyThrows
public String login() { public String login() {
try {
Map<String, String> requestBody = new HashMap<>(); Map<String, String> requestBody = new HashMap<>();
requestBody.put("nickname", "changzhu-4"); requestBody.put("nickname", "changzhu-4");
requestBody.put("password", "123456QWe@"); requestBody.put("password", "123456QWe@");
@@ -79,11 +84,14 @@ public class GenmaiServiceImpl {
String body = response.getBody(); String body = response.getBody();
JsonNode root = objectMapper.readTree(body); JsonNode root = objectMapper.readTree(body);
requestBody.clear(); requestBody.clear();
String token= root.get("result").asText(); String token = root.get("result").asText();
HttpEntity<String> updateEntity = new HttpEntity<>(token,headers); HttpEntity<String> updateEntity = new HttpEntity<>(token, headers);
ResponseEntity<String> exchange = restTemplate.exchange(serverApiUrl + updateGenmaijlToken, HttpMethod.POST, updateEntity, String.class); ResponseEntity<String> exchange = restTemplate.exchange(serverApiUrl + updateGenmaijlToken, HttpMethod.POST, updateEntity, String.class);
System.out.println(exchange.getBody()); System.out.println(exchange.getBody());
return token; return token;
} catch (Exception e) {
throw new RuntimeException("跟卖精灵登录失败", e);
}
} }
} }

View File

@@ -194,7 +194,6 @@ public class DeviceUtils {
return "SYS_" + SecureUtil.md5(combined).substring(0, 16).toUpperCase(); return "SYS_" + SecureUtil.md5(combined).substring(0, 16).toUpperCase();
} catch (Exception e) { } catch (Exception e) {
log.error("获取系统信息异常: {}", e.getMessage()); log.error("获取系统信息异常: {}", e.getMessage());
// 最终的最终降级时间戳哈希不推荐但保证不返回null
String fallbackId = "FALLBACK_" + SecureUtil.md5(String.valueOf(System.currentTimeMillis())).substring(0, 16).toUpperCase(); String fallbackId = "FALLBACK_" + SecureUtil.md5(String.valueOf(System.currentTimeMillis())).substring(0, 16).toUpperCase();
log.warn("使用最终降级方案时间戳哈希设备ID: {}", fallbackId); log.warn("使用最终降级方案时间戳哈希设备ID: {}", fallbackId);
return fallbackId; return fallbackId;

View File

@@ -259,7 +259,7 @@ public class ClientAccountController extends BaseController {
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}"); clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
clientAccount.setPassword(passwordEncoder.encode(password)); clientAccount.setPassword(passwordEncoder.encode(password));
clientAccount.setAccountType("trial"); clientAccount.setAccountType("trial");
clientAccount.setDeviceLimit(3); clientAccount.setDeviceLimit(1);
clientAccount.setExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000)); clientAccount.setExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
int result = clientAccountService.insertClientAccount(clientAccount); int result = clientAccountService.insertClientAccount(clientAccount);

View File

@@ -27,6 +27,10 @@ public class ClientDeviceController {
return accountMapper.selectClientAccountByUsername(username); return accountMapper.selectClientAccountByUsername(username);
} }
private int getDeviceLimit(ClientAccount account) {
return account.getDeviceLimit() != null ? account.getDeviceLimit() : 1;
}
private boolean exceedDeviceLimit(Long accountId, String deviceId, int limit) { private boolean exceedDeviceLimit(Long accountId, String deviceId, int limit) {
int count = accountDeviceMapper.countActiveDevicesByAccountId(accountId); int count = accountDeviceMapper.countActiveDevicesByAccountId(accountId);
ClientAccountDevice binding = accountDeviceMapper.selectByAccountIdAndDeviceId(accountId, deviceId); ClientAccountDevice binding = accountDeviceMapper.selectByAccountIdAndDeviceId(accountId, deviceId);
@@ -37,22 +41,25 @@ public class ClientDeviceController {
@GetMapping("/quota") @GetMapping("/quota")
public AjaxResult quota(@RequestParam String username) { public AjaxResult quota(@RequestParam String username) {
ClientAccount account = getAccount(username); ClientAccount account = getAccount(username);
if (account == null) return AjaxResult.error("账号不存在");
int used = accountDeviceMapper.countActiveDevicesByAccountId(account.getId()); int used = accountDeviceMapper.countActiveDevicesByAccountId(account.getId());
int limit = account.getDeviceLimit() != null ? account.getDeviceLimit() : 3; return AjaxResult.success(Map.of("limit", getDeviceLimit(account), "used", used));
return AjaxResult.success(Map.of("limit", limit, "used", used));
} }
@GetMapping("/list") @GetMapping("/list")
public AjaxResult list(@RequestParam String username) { public AjaxResult list(@RequestParam String username) {
return AjaxResult.success(accountDeviceMapper.selectDevicesByAccountId(getAccount(username).getId())); ClientAccount account = getAccount(username);
if (account == null) return AjaxResult.error("账号不存在");
return AjaxResult.success(accountDeviceMapper.selectDevicesByAccountId(account.getId()));
} }
@PostMapping("/register") @PostMapping("/register")
public AjaxResult register(@RequestBody Map<String, String> data) { public AjaxResult register(@RequestBody Map<String, String> data) {
String username = data.get("username"); String username = data.get("username");
String deviceId = data.get("deviceId"); String deviceId = data.get("deviceId");
ClientAccount account = getAccount(username); ClientAccount account = getAccount(username);
int limit = account.getDeviceLimit() != null ? account.getDeviceLimit() : 3; if (account == null) return AjaxResult.error("账号不存在");
int limit = getDeviceLimit(account);
if (exceedDeviceLimit(account.getId(), deviceId, limit)) { if (exceedDeviceLimit(account.getId(), deviceId, limit)) {
return AjaxResult.error("设备数量已达上限(" + limit + "个)"); return AjaxResult.error("设备数量已达上限(" + limit + "个)");
} }
@@ -62,17 +69,14 @@ public class ClientDeviceController {
if (device == null) { if (device == null) {
device = new ClientDevice(); device = new ClientDevice();
device.setDeviceId(deviceId); device.setDeviceId(deviceId);
device.setName(username + "@" + data.get("ip") + " (" + data.get("os") + ")");
device.setOs(data.get("os"));
device.setIp(data.get("ip"));
device.setLocation(data.get("location"));
device.setTrialExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000)); device.setTrialExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
deviceMapper.insert(device); deviceMapper.insert(device);
} }
device.setName(username + "@" + data.get("computerName") + " (" + data.get("os") + ")");
device.setOs(data.get("os"));
device.setIp(data.get("ip"));
device.setStatus("online"); device.setStatus("online");
device.setLastActiveAt(now); device.setLastActiveAt(now);
device.setIp(data.get("ip"));
device.setLocation(data.get("location"));
deviceMapper.updateByDeviceId(device); deviceMapper.updateByDeviceId(device);
ClientAccountDevice binding = accountDeviceMapper.selectByAccountIdAndDeviceId(account.getId(), deviceId); ClientAccountDevice binding = accountDeviceMapper.selectByAccountIdAndDeviceId(account.getId(), deviceId);
@@ -90,14 +94,8 @@ public class ClientDeviceController {
return AjaxResult.success(); return AjaxResult.success();
} }
@PostMapping("/rename") @PostMapping("/update")
public AjaxResult rename(@RequestBody ClientDevice device) { public AjaxResult update(@RequestBody ClientDevice device) {
deviceMapper.updateByDeviceId(device);
return AjaxResult.success();
}
@PostMapping("/updateExpire")
public AjaxResult updateExpire(@RequestBody ClientDevice device) {
deviceMapper.updateByDeviceId(device); deviceMapper.updateByDeviceId(device);
return AjaxResult.success(); return AjaxResult.success();
} }
@@ -106,9 +104,10 @@ public class ClientDeviceController {
public AjaxResult remove(@RequestBody Map<String, String> data) { public AjaxResult remove(@RequestBody Map<String, String> data) {
String deviceId = data.get("deviceId"); String deviceId = data.get("deviceId");
String username = data.get("username"); String username = data.get("username");
Long accountId = getAccount(username).getId(); ClientAccount account = getAccount(username);
if (account == null) return AjaxResult.error("账号不存在");
accountDeviceMapper.updateStatus(accountId, deviceId, "removed"); accountDeviceMapper.updateStatus(account.getId(), deviceId, "removed");
sseHubService.sendEvent(username, deviceId, "DEVICE_REMOVED", "{}"); sseHubService.sendEvent(username, deviceId, "DEVICE_REMOVED", "{}");
sseHubService.disconnectDevice(username, deviceId); sseHubService.disconnectDevice(username, deviceId);
return AjaxResult.success(); return AjaxResult.success();
@@ -125,5 +124,3 @@ public class ClientDeviceController {
return AjaxResult.success(); return AjaxResult.success();
} }
} }

View File

@@ -79,10 +79,10 @@ export function getDeviceList(username) {
}) })
} }
// 修改设备试用期过期时间 // 更新设备信息
export function updateDeviceExpire(data) { export function updateDeviceExpire(data) {
return request({ return request({
url: '/monitor/device/updateExpire', url: '/monitor/device/update',
method: 'post', method: 'post',
data: data data: data
}) })

View File

@@ -148,16 +148,16 @@
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body> <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px"> <el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="账号名称" prop="accountName"> <el-form-item label="账号名称" prop="accountName">
<el-input v-model="form.accountName" placeholder="请输入账号名称" /> <el-input v-model="form.accountName" placeholder="请输入账号名称" :disabled="!!form.id" />
</el-form-item> </el-form-item>
<el-form-item label="用户名" prop="username"> <el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" /> <el-input v-model="form.username" placeholder="请输入用户名" :disabled="!!form.id" />
</el-form-item> </el-form-item>
<el-form-item label="密码" prop="password" v-if="!form.id"> <el-form-item label="密码" prop="password" v-if="!form.id">
<el-input v-model="form.password" type="password" placeholder="请输入密码" /> <el-input v-model="form.password" type="password" placeholder="请输入密码" />
</el-form-item> </el-form-item>
<el-form-item label="账号状态"> <el-form-item label="账号状态">
<el-radio-group v-model="form.status"> <el-radio-group v-model="form.status" :disabled="!!form.id">
<el-radio <el-radio
v-for="dict in statusOptions" v-for="dict in statusOptions"
:key="dict.dictValue" :key="dict.dictValue"
@@ -340,6 +340,7 @@ export default {
deviceListLoading: false, deviceListLoading: false,
deviceList: [], deviceList: [],
currentAccountName: '', currentAccountName: '',
currentUsername: '', // 当前账号的用户名
// 设备过期时间编辑 // 设备过期时间编辑
deviceExpireDialogVisible: false, deviceExpireDialogVisible: false,
currentDevice: {}, currentDevice: {},
@@ -598,6 +599,7 @@ export default {
/** 查看设备列表 */ /** 查看设备列表 */
async viewDevices(row) { async viewDevices(row) {
this.currentAccountName = row.accountName || row.username; this.currentAccountName = row.accountName || row.username;
this.currentUsername = row.username;
this.deviceListVisible = true; this.deviceListVisible = true;
this.deviceListLoading = true; this.deviceListLoading = true;
try { try {
@@ -620,13 +622,12 @@ export default {
try { try {
await updateDeviceExpire({ await updateDeviceExpire({
deviceId: this.currentDevice.deviceId, deviceId: this.currentDevice.deviceId,
username: this.currentDevice.username,
trialExpireTime: this.currentDevice.trialExpireTime trialExpireTime: this.currentDevice.trialExpireTime
}); });
this.$modal.msgSuccess('修改成功'); this.$modal.msgSuccess('修改成功');
this.deviceExpireDialogVisible = false; this.deviceExpireDialogVisible = false;
// 刷新设备列表 // 刷新设备列表(使用保存的 username
const response = await getDeviceList(this.currentDevice.username); const response = await getDeviceList(this.currentUsername);
this.deviceList = response.data || []; this.deviceList = response.data || [];
} catch (error) { } catch (error) {
this.$modal.msgError('修改失败: ' + (error.message || '未知错误')); this.$modal.msgError('修改失败: ' + (error.message || '未知错误'));