feat(electron-vue-template):重构认证与设备管理模块

- 统一token存取逻辑,封装getToken/setToken/removeToken方法
-优化设备ID获取逻辑,调整API路径
- 完善设备管理接口类型定义,增强类型安全
- 调整SSE连接逻辑,使用统一配置管理- 重构HTTP客户端,集中管理后端服务配置
- 更新认证相关API接口,完善请求/响应类型
- 优化设备列表展示逻辑,移除冗余字段
- 调整图片代理路径,统一API前缀
- 完善用户反馈列表展示功能,增强交互体验
- 移除冗余的错误处理逻辑,简化代码结构
This commit is contained in:
2025-10-16 10:37:00 +08:00
parent 6f04658265
commit 132299c4b7
37 changed files with 2193 additions and 682 deletions

View File

@@ -1,55 +0,0 @@
package com.tashow.erp.controller;
import com.tashow.erp.entity.AuthTokenEntity;
import com.tashow.erp.repository.AuthTokenRepository;
import com.tashow.erp.utils.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 客户端本地服务控制器
*/
@RestController
@RequestMapping("/api")
public class AuthController {
@Autowired
private AuthTokenRepository authTokenRepository;
/**
* 保存认证密钥
*/
@PostMapping("/auth/save")
public JsonData saveAuth(@RequestBody Map<String, Object> data) {
String serviceName = (String) data.get("serviceName");
String authKey = (String) data.get("authKey");
if (serviceName == null || authKey == null) return JsonData.buildError("serviceName和authKey不能为空");
AuthTokenEntity entity = authTokenRepository.findByServiceName(serviceName).orElse(new AuthTokenEntity());
entity.setServiceName(serviceName);
entity.setToken(authKey);
authTokenRepository.save(entity);
return JsonData.buildSuccess("认证信息保存成功");
}
@GetMapping("/auth/get")
public JsonData getAuth(@RequestParam String serviceName) {
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null));
}
/**
* 删除认证密钥
*/
@DeleteMapping("/auth/remove")
public JsonData removeAuth(@RequestParam String serviceName) {
authTokenRepository.findByServiceName(serviceName).ifPresent(authTokenRepository::delete);
return JsonData.buildSuccess("认证信息删除成功");
}
/**
* 获取设备ID
*/
@GetMapping("/device-id")
public JsonData getDeviceId() {
String deviceId = com.tashow.erp.utils.DeviceUtils.generateDeviceId();
return JsonData.buildSuccess(deviceId);
}
}

View File

@@ -1,30 +0,0 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 配置信息控制器
*/
@RestController
@RequestMapping("/api/config")
public class ConfigController {
@Value("${api.server.base-url}")
private String serverBaseUrl;
/**
* 获取服务器配置
*/
@GetMapping("/server")
public Map<String, Object> getServerConfig() {
return Map.of(
"baseUrl", serverBaseUrl,
"sseUrl", serverBaseUrl + "/monitor/account/events"
);
}
}

View File

@@ -1,25 +0,0 @@
package com.tashow.erp.controller;
import com.tashow.erp.service.IGenmaiService;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/genmai")
public class GenmaiController {
private static final Logger logger = LoggerUtil.getLogger(GenmaiController.class);
@Autowired
private IGenmaiService genmaiService;
/**
* 打开跟卖精灵网页
*/
@PostMapping("/open")
public void openGenmaiWebsite() {
genmaiService.openGenmaiWebsite();
}
}

View File

@@ -1,18 +0,0 @@
package com.tashow.erp.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "redirect:/html/erp-dashboard.html";
}
@GetMapping("/erp")
public String erp() {
return "redirect:/html/erp-dashboard.html";
}
}

View File

@@ -1,116 +0,0 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* 代理控制器用于解决CORS跨域问题
*/
@RestController
@RequestMapping("/api/proxy")
public class ProxyController {
@Autowired
private RestTemplate restTemplate;
/**
* 代理获取图片
* @param requestBody 包含图片URL的请求体
* @return 图片字节数组
*/
@PostMapping("/image")
public ResponseEntity<byte[]> proxyImage(@RequestBody Map<String, String> requestBody) {
String imageUrl = requestBody.get("imageUrl");
if (imageUrl == null || imageUrl.isEmpty()) {
return ResponseEntity.badRequest().build();
}
try {
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
HttpEntity<String> entity = new HttpEntity<>(headers);
// 发送请求获取图片
ResponseEntity<byte[]> response = restTemplate.exchange(
imageUrl,
HttpMethod.GET,
entity,
byte[].class
);
// 设置响应头
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 通过URL参数代理获取图片
* @param imageUrl 图片URL
* @return 图片字节数组
*/
@GetMapping("/image-url")
public ResponseEntity<byte[]> proxyImageByUrl(@RequestParam("url") String imageUrl) {
if (imageUrl == null || imageUrl.isEmpty()) {
System.err.println("图片代理请求失败: 图片URL为空");
return ResponseEntity.badRequest().build();
}
System.out.println("代理图片请求: " + imageUrl);
try {
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
headers.set("Referer", "https://item.rakuten.co.jp/");
HttpEntity<String> entity = new HttpEntity<>(headers);
// 发送请求获取图片
ResponseEntity<byte[]> response = restTemplate.exchange(
imageUrl,
HttpMethod.GET,
entity,
byte[].class
);
System.out.println("图片代理成功,响应大小: " + (response.getBody() != null ? response.getBody().length : 0) + " bytes");
// 设置响应头支持缓存以提升JavaFX WebView性能
HttpHeaders responseHeaders = new HttpHeaders();
// 尝试从原始响应中获取Content-Type
String contentType = response.getHeaders().getFirst("Content-Type");
if (contentType != null && contentType.startsWith("image/")) {
responseHeaders.setContentType(MediaType.parseMediaType(contentType));
} else {
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
}
// 设置缓存头以提升性能
responseHeaders.setCacheControl("max-age=3600");
// 删除手动CORS设置使用WebConfig中的全局CORS配置
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {
System.err.println("图片代理失败: " + imageUrl + " - " + e.getMessage());
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

View File

@@ -0,0 +1,172 @@
package com.tashow.erp.controller;
import com.tashow.erp.entity.AuthTokenEntity;
import com.tashow.erp.repository.AuthTokenRepository;
import com.tashow.erp.service.IGenmaiService;
import com.tashow.erp.utils.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* 系统级接口控制器
* 整合:认证、配置、版本、工具、代理等功能
*/
@RestController
@RequestMapping("/api/system")
public class SystemController {
@Autowired
private AuthTokenRepository authTokenRepository;
@Autowired
private IGenmaiService genmaiService;
@Autowired
private RestTemplate restTemplate;
@Value("${project.version:2.3.6}")
private String currentVersion;
@Value("${project.build.time:}")
private String buildTime;
@Value("${api.server.base-url}")
private String serverBaseUrl;
// ==================== 认证管理 ====================
/**
* 保存认证密钥
*/
@PostMapping("/auth/save")
public JsonData saveAuth(@RequestBody Map<String, Object> data) {
String serviceName = (String) data.get("serviceName");
String authKey = (String) data.get("authKey");
if (serviceName == null || authKey == null) {
return JsonData.buildError("serviceName和authKey不能为空");
}
AuthTokenEntity entity = authTokenRepository.findByServiceName(serviceName)
.orElse(new AuthTokenEntity());
entity.setServiceName(serviceName);
entity.setToken(authKey);
authTokenRepository.save(entity);
return JsonData.buildSuccess("认证信息保存成功");
}
/**
* 获取认证密钥
*/
@GetMapping("/auth/get")
public JsonData getAuth(@RequestParam String serviceName) {
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName)
.map(AuthTokenEntity::getToken)
.orElse(null));
}
/**
* 删除认证密钥
*/
@DeleteMapping("/auth/remove")
public JsonData removeAuth(@RequestParam String serviceName) {
authTokenRepository.findByServiceName(serviceName)
.ifPresent(authTokenRepository::delete);
return JsonData.buildSuccess("认证信息删除成功");
}
// ==================== 设备管理 ====================
/**
* 获取设备ID
*/
@GetMapping("/device-id")
public JsonData getDeviceId() {
String deviceId = com.tashow.erp.utils.DeviceUtils.generateDeviceId();
return JsonData.buildSuccess(deviceId);
}
// ==================== 版本信息 ====================
/**
* 获取当前版本号
*/
@GetMapping("/version")
public Map<String, Object> getVersion() {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("currentVersion", currentVersion);
result.put("buildTime", buildTime);
return result;
}
// ==================== 配置信息 ====================
/**
* 获取服务器配置
*/
@GetMapping("/config/server")
public Map<String, Object> getServerConfig() {
return Map.of(
"baseUrl", serverBaseUrl,
"sseUrl", serverBaseUrl + "/monitor/account/events"
);
}
// ==================== 工具功能 ====================
/**
* 打开跟卖精灵网页
*/
@PostMapping("/genmai/open")
public void openGenmaiWebsite() {
genmaiService.openGenmaiWebsite();
}
// ==================== 图片代理 ====================
/**
* 代理获取图片解决CORS跨域问题
*/
@GetMapping("/proxy/image")
public ResponseEntity<byte[]> proxyImage(@RequestParam("url") String imageUrl) {
if (imageUrl == null || imageUrl.isEmpty()) {
return ResponseEntity.badRequest().build();
}
try {
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
headers.set("Referer", "https://item.rakuten.co.jp/");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<byte[]> response = restTemplate.exchange(
imageUrl,
HttpMethod.GET,
entity,
byte[].class
);
HttpHeaders responseHeaders = new HttpHeaders();
String contentType = response.getHeaders().getFirst("Content-Type");
if (contentType != null && contentType.startsWith("image/")) {
responseHeaders.setContentType(MediaType.parseMediaType(contentType));
} else {
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
}
responseHeaders.setCacheControl("max-age=3600");
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

View File

@@ -1,33 +0,0 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* Web版本信息控制器
*
* @author Claude
*/
@RestController
@RequestMapping("/api/update")
public class UpdateController {
@Value("${project.version:2.3.6}")
private String currentVersion;
@Value("${project.build.time:}")
private String buildTime;
/**
* 获取当前版本号
*
* @return 当前版本号
*/
@GetMapping("/version")
public Map<String, Object> getVersion() {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("currentVersion", currentVersion);
result.put("buildTime", buildTime);
return result;
}
}

View File

@@ -0,0 +1,47 @@
package com.tashow.erp.test;
import com.tashow.erp.utils.DeviceUtils;
/**
* 设备ID获取测试
* 独立运行,不依赖 Spring Boot
*/
public class DeviceIdTest {
public static void main(String[] args) {
System.out.println("=================================");
System.out.println("设备ID获取测试");
System.out.println("=================================\n");
try {
String deviceId = DeviceUtils.generateDeviceId();
System.out.println("✓ 成功获取设备ID: " + deviceId);
System.out.println("\n设备ID格式说明");
System.out.println(" MGUID_ - Windows MachineGuid最可靠");
System.out.println(" HW_ - 硬件UUID");
System.out.println(" CPU_ - 处理器ID");
System.out.println(" MB_ - 主板序列号");
System.out.println(" MAC_ - MAC地址");
System.out.println(" SYS_ - 系统信息组合");
// 再次获取,验证稳定性
System.out.println("\n验证稳定性再次获取");
String deviceId2 = DeviceUtils.generateDeviceId();
System.out.println("第二次获取: " + deviceId2);
if (deviceId.equals(deviceId2)) {
System.out.println("✓ 设备ID稳定两次获取结果一致");
} else {
System.out.println("✗ 警告设备ID不稳定两次获取结果不同");
}
} catch (Exception e) {
System.err.println("✗ 获取设备ID失败");
e.printStackTrace();
}
System.out.println("\n=================================");
System.out.println("测试完成");
System.out.println("=================================");
}
}

View File

@@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
public class aa {
@GetMapping("/a")
@GetMapping("/aa")
public String aa() {
DeviceUtils deviceUtils = new DeviceUtils();
return deviceUtils.generateDeviceId();

View File

@@ -24,8 +24,6 @@ public class DeviceUtils {
public static String generateDeviceId() {
String deviceId = null;
log.info("========== 开始生成设备ID ==========");
// 策略1: Windows MachineGuid注册表
deviceId = getMachineGuid();
if (deviceId != null) return deviceId;

View File

@@ -3,6 +3,24 @@
<!-- 固定日志路径到系统公共数据目录 -->
<property name="LOG_HOME" value="C:/ProgramData/erp-logs" />
<!--
日志上报说明:
日志文件按天滚动存储在 ${LOG_HOME} 目录下
格式spring-boot-yyyy-MM-dd.log
用户反馈系统集成说明:
- 客户端应用Electron可以读取本地日志文件
- 用户在"设置-反馈"页面提交反馈时,可选择附带某一天的日志文件
- 日志文件将随反馈内容一起上传到服务器
- 服务器存储路径C:/ProgramData/erp-logs/feedback/ (Windows) 或 /opt/erp/feedback-logs/ (Linux)
- 管理员可在后台管理界面查看反馈并下载相应日志文件
日志保留策略:
- 本地日志保留30天
- 总大小限制1GB
- 超过限制时自动删除最旧的日志文件
-->
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>