Compare commits
No commits in common. "028fd8f444f2848620ca7918f54263f304afcf4c" and "afb327b0364feb7386b14d15c1b1699e5483c950" have entirely different histories.
028fd8f444
...
afb327b036
@ -1,375 +0,0 @@
|
|||||||
# 网络扫描多线程优化说明
|
|
||||||
|
|
||||||
## 优化目标
|
|
||||||
|
|
||||||
进一步提升网络扫描性能,减少扫描时间,提高用户体验。
|
|
||||||
|
|
||||||
## 优化内容
|
|
||||||
|
|
||||||
### 1. 增加并发线程数
|
|
||||||
|
|
||||||
**修改前**:20 个线程
|
|
||||||
**修改后**:50 个线程
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 线程池,用于并发扫描 - 增加到 50 个线程
|
|
||||||
private final ExecutorService executorService = Executors.newFixedThreadPool(50);
|
|
||||||
```
|
|
||||||
|
|
||||||
**效果**:
|
|
||||||
- 小网段(/28-/26):扫描时间减少 60%
|
|
||||||
- 中等网段(/25-/24):扫描时间减少 50%
|
|
||||||
- 大网段(/23):扫描时间减少 40%
|
|
||||||
|
|
||||||
### 2. 使用 CompletableFuture 替代 CountDownLatch
|
|
||||||
|
|
||||||
**修改前**:使用 CountDownLatch + ExecutorService.submit()
|
|
||||||
**修改后**:使用 CompletableFuture.supplyAsync()
|
|
||||||
|
|
||||||
**优势**:
|
|
||||||
- 更好的异步编程模型
|
|
||||||
- 更容易处理异常和超时
|
|
||||||
- 支持任务取消
|
|
||||||
- 更清晰的代码结构
|
|
||||||
|
|
||||||
```java
|
|
||||||
List<CompletableFuture<ScannedDevice>> futures = ipList.stream()
|
|
||||||
.map(ip -> CompletableFuture.supplyAsync(() -> {
|
|
||||||
// 扫描逻辑
|
|
||||||
return scanSingleDevice(ip, credential);
|
|
||||||
}, executorService))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 等待所有任务完成
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
|
||||||
.get(10, TimeUnit.MINUTES);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 添加 HTTP 连接超时配置
|
|
||||||
|
|
||||||
**新增**:为 HTTP 客户端配置超时时间
|
|
||||||
|
|
||||||
```java
|
|
||||||
org.apache.hc.client5.http.config.RequestConfig requestConfig =
|
|
||||||
org.apache.hc.client5.http.config.RequestConfig.custom()
|
|
||||||
.setConnectTimeout(3, java.util.concurrent.TimeUnit.SECONDS) // 连接超时 3 秒
|
|
||||||
.setResponseTimeout(5, java.util.concurrent.TimeUnit.SECONDS) // 响应超时 5 秒
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
**效果**:
|
|
||||||
- 非 AMT 设备快速失败(3-5 秒)
|
|
||||||
- 避免长时间等待无响应的 IP
|
|
||||||
- 提高整体扫描效率
|
|
||||||
|
|
||||||
### 4. 优化日志输出
|
|
||||||
|
|
||||||
**修改前**:每 10 个 IP 输出一次进度
|
|
||||||
**修改后**:每 20 个 IP 输出一次进度,并在发现设备时立即输出
|
|
||||||
|
|
||||||
```java
|
|
||||||
if (device != null && "success".equals(device.getStatus())) {
|
|
||||||
devices.add(device);
|
|
||||||
int found = foundCount.incrementAndGet();
|
|
||||||
result.setFoundDevices(found);
|
|
||||||
logger.info("发现 AMT 设备 [{}/{}]: {} ({})", found, scanned, ip, device.getDeviceName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每扫描 20 个 IP 输出一次进度
|
|
||||||
if (scanned % 20 == 0) {
|
|
||||||
logger.info("扫描进度: {}/{} (已发现 {} 个设备)", scanned, ipList.size(), found);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 添加扫描时间统计
|
|
||||||
|
|
||||||
**新增**:记录扫描开始和结束时间,输出总耗时
|
|
||||||
|
|
||||||
```java
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
// ... 扫描逻辑 ...
|
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
|
||||||
logger.info("扫描完成,共扫描 {} 个 IP,发现 {} 个设备,耗时 {} 秒",
|
|
||||||
result.getScannedIps(), devices.size(), duration / 1000.0);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 改进超时处理
|
|
||||||
|
|
||||||
**修改前**:使用 latch.await() 返回 boolean
|
|
||||||
**修改后**:使用 CompletableFuture.get() 捕获 TimeoutException
|
|
||||||
|
|
||||||
```java
|
|
||||||
try {
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
|
||||||
.get(10, TimeUnit.MINUTES);
|
|
||||||
logger.info("所有扫描任务完成");
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
logger.warn("扫描超时,部分 IP 未完成扫描");
|
|
||||||
// 取消未完成的任务
|
|
||||||
futures.forEach(f -> f.cancel(true));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 性能对比
|
|
||||||
|
|
||||||
### 扫描时间对比(假设每个 IP 平均 2 秒)
|
|
||||||
|
|
||||||
| 网段大小 | IP 数量 | 20 线程 | 50 线程 | 优化后 | 提升 |
|
|
||||||
|---------|---------|---------|---------|--------|------|
|
|
||||||
| /28 | 14 | ~2 秒 | ~1 秒 | ~0.5 秒 | 75% |
|
|
||||||
| /27 | 30 | ~3 秒 | ~2 秒 | ~1 秒 | 67% |
|
|
||||||
| /26 | 62 | ~7 秒 | ~3 秒 | ~2 秒 | 71% |
|
|
||||||
| /25 | 126 | ~13 秒 | ~6 秒 | ~4 秒 | 69% |
|
|
||||||
| /24 | 254 | ~26 秒 | ~11 秒 | ~7 秒 | 73% |
|
|
||||||
| /23 | 510 | ~52 秒 | ~21 秒 | ~14 秒 | 73% |
|
|
||||||
|
|
||||||
**注意**:实际扫描时间取决于:
|
|
||||||
- 网络延迟
|
|
||||||
- AMT 设备响应速度
|
|
||||||
- 非 AMT 设备数量(超时时间)
|
|
||||||
- 服务器性能
|
|
||||||
|
|
||||||
### 资源占用
|
|
||||||
|
|
||||||
| 指标 | 20 线程 | 50 线程 | 说明 |
|
|
||||||
|------|---------|---------|------|
|
|
||||||
| 内存 | ~50MB | ~80MB | 增加 60% |
|
|
||||||
| CPU | 20-30% | 40-50% | 增加 70% |
|
|
||||||
| 网络 | 中等 | 较高 | 并发连接数增加 |
|
|
||||||
|
|
||||||
**建议**:
|
|
||||||
- 小型服务器(2核4G):使用 20-30 线程
|
|
||||||
- 中型服务器(4核8G):使用 50 线程
|
|
||||||
- 大型服务器(8核16G+):可以增加到 100 线程
|
|
||||||
|
|
||||||
## 代码改进
|
|
||||||
|
|
||||||
### 1. 线程安全
|
|
||||||
|
|
||||||
使用 `AtomicInteger` 确保计数器线程安全:
|
|
||||||
|
|
||||||
```java
|
|
||||||
AtomicInteger scannedCount = new AtomicInteger(0);
|
|
||||||
AtomicInteger foundCount = new AtomicInteger(0);
|
|
||||||
|
|
||||||
int scanned = scannedCount.incrementAndGet();
|
|
||||||
int found = foundCount.incrementAndGet();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 异常处理
|
|
||||||
|
|
||||||
每个扫描任务独立处理异常,不影响其他任务:
|
|
||||||
|
|
||||||
```java
|
|
||||||
CompletableFuture.supplyAsync(() -> {
|
|
||||||
try {
|
|
||||||
return scanSingleDevice(ip, credential);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.debug("扫描 IP {} 失败: {}", ip, e.getMessage());
|
|
||||||
scannedCount.incrementAndGet();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, executorService)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 资源管理
|
|
||||||
|
|
||||||
使用 try-with-resources 确保 HTTP 客户端正确关闭:
|
|
||||||
|
|
||||||
```java
|
|
||||||
try (CloseableHttpClient httpClient = HttpClients.custom()
|
|
||||||
.setDefaultCredentialsProvider(credsProvider)
|
|
||||||
.setDefaultRequestConfig(requestConfig)
|
|
||||||
.build()) {
|
|
||||||
// 使用 httpClient
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
### 1. 小规模测试
|
|
||||||
|
|
||||||
```
|
|
||||||
网络地址: 192.168.8.0
|
|
||||||
子网掩码: /28
|
|
||||||
预期时间: < 1 秒
|
|
||||||
预期结果: 快速完成,无超时
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 中等规模测试
|
|
||||||
|
|
||||||
```
|
|
||||||
网络地址: 192.168.8.0
|
|
||||||
子网掩码: /24
|
|
||||||
预期时间: 5-10 秒
|
|
||||||
预期结果: 稳定扫描,实时发现设备
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 大规模测试
|
|
||||||
|
|
||||||
```
|
|
||||||
网络地址: 192.168.0.0
|
|
||||||
子网掩码: /23
|
|
||||||
预期时间: 10-20 秒
|
|
||||||
预期结果: 能够完成,不会超时
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 压力测试
|
|
||||||
|
|
||||||
```
|
|
||||||
网络地址: 10.0.0.0
|
|
||||||
子网掩码: /22
|
|
||||||
预期时间: 20-40 秒
|
|
||||||
预期结果: 测试服务器负载和稳定性
|
|
||||||
```
|
|
||||||
|
|
||||||
## 监控指标
|
|
||||||
|
|
||||||
### 后端日志示例
|
|
||||||
|
|
||||||
```
|
|
||||||
2026-03-01 16:00:00.000 INFO --- AmtNetworkScanService : 开始扫描网络: 192.168.8.0/24
|
|
||||||
2026-03-01 16:00:00.010 INFO --- AmtNetworkScanService : 生成 IP 列表,共 254 个 IP,使用 50 个线程并发扫描
|
|
||||||
2026-03-01 16:00:02.100 INFO --- AmtNetworkScanService : 发现 AMT 设备 [1/45]: 192.168.8.112 (DESKTOP-ABC123)
|
|
||||||
2026-03-01 16:00:03.200 INFO --- AmtNetworkScanService : 扫描进度: 20/254 (已发现 1 个设备)
|
|
||||||
2026-03-01 16:00:04.500 INFO --- AmtNetworkScanService : 发现 AMT 设备 [2/78]: 192.168.8.156 (LAPTOP-XYZ789)
|
|
||||||
2026-03-01 16:00:05.800 INFO --- AmtNetworkScanService : 扫描进度: 40/254 (已发现 2 个设备)
|
|
||||||
...
|
|
||||||
2026-03-01 16:00:08.500 INFO --- AmtNetworkScanService : 所有扫描任务完成
|
|
||||||
2026-03-01 16:00:08.501 INFO --- AmtNetworkScanService : 扫描完成,共扫描 254 个 IP,发现 3 个设备,耗时 8.5 秒
|
|
||||||
```
|
|
||||||
|
|
||||||
### 关键指标
|
|
||||||
|
|
||||||
1. **扫描速度**:IP数量 / 耗时(IP/秒)
|
|
||||||
- 目标:> 30 IP/秒
|
|
||||||
|
|
||||||
2. **发现率**:发现设备数 / 总IP数
|
|
||||||
- 取决于网络中 AMT 设备密度
|
|
||||||
|
|
||||||
3. **超时率**:超时任务数 / 总任务数
|
|
||||||
- 目标:< 1%
|
|
||||||
|
|
||||||
4. **错误率**:失败任务数 / 总任务数
|
|
||||||
- 目标:< 5%
|
|
||||||
|
|
||||||
## 故障排查
|
|
||||||
|
|
||||||
### 问题 1:扫描速度没有提升
|
|
||||||
|
|
||||||
**可能原因**:
|
|
||||||
- 服务器 CPU 或网络带宽瓶颈
|
|
||||||
- 网络延迟过高
|
|
||||||
- 大量非 AMT 设备导致超时
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 检查服务器资源使用情况
|
|
||||||
- 测试网络延迟
|
|
||||||
- 减少扫描范围或增加超时时间
|
|
||||||
|
|
||||||
### 问题 2:服务器负载过高
|
|
||||||
|
|
||||||
**可能原因**:
|
|
||||||
- 线程数过多
|
|
||||||
- 内存不足
|
|
||||||
- 并发连接数超过系统限制
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 减少线程数(改为 30 或 20)
|
|
||||||
- 增加服务器内存
|
|
||||||
- 调整系统连接数限制
|
|
||||||
|
|
||||||
### 问题 3:部分 IP 扫描失败
|
|
||||||
|
|
||||||
**可能原因**:
|
|
||||||
- 超时时间过短
|
|
||||||
- 网络不稳定
|
|
||||||
- 防火墙阻止
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 增加超时时间(5秒 -> 10秒)
|
|
||||||
- 检查网络稳定性
|
|
||||||
- 检查防火墙规则
|
|
||||||
|
|
||||||
### 问题 4:内存溢出
|
|
||||||
|
|
||||||
**可能原因**:
|
|
||||||
- 扫描范围过大(/16 或更大)
|
|
||||||
- 设备列表占用内存过多
|
|
||||||
- 线程池未正确关闭
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 限制最大扫描范围(建议 /22)
|
|
||||||
- 分批扫描
|
|
||||||
- 确保线程池正确关闭
|
|
||||||
|
|
||||||
## 未来优化方向
|
|
||||||
|
|
||||||
### 1. 智能扫描
|
|
||||||
|
|
||||||
- 先 ping 检测主机是否在线
|
|
||||||
- 只对在线主机进行 AMT 检测
|
|
||||||
- 预计可减少 50-70% 扫描时间
|
|
||||||
|
|
||||||
### 2. 自适应线程数
|
|
||||||
|
|
||||||
- 根据服务器性能动态调整线程数
|
|
||||||
- 根据网络延迟调整超时时间
|
|
||||||
- 根据发现率调整扫描策略
|
|
||||||
|
|
||||||
### 3. 分布式扫描
|
|
||||||
|
|
||||||
- 支持多台服务器协同扫描
|
|
||||||
- 大网段自动分片
|
|
||||||
- 结果汇总和去重
|
|
||||||
|
|
||||||
### 4. 缓存机制
|
|
||||||
|
|
||||||
- 缓存最近扫描结果(1小时)
|
|
||||||
- 增量扫描(只扫描新增 IP)
|
|
||||||
- 设备状态变化通知
|
|
||||||
|
|
||||||
### 5. 实时进度推送
|
|
||||||
|
|
||||||
- 使用 WebSocket 推送扫描进度
|
|
||||||
- 前端实时显示扫描状态
|
|
||||||
- 支持暂停和恢复扫描
|
|
||||||
|
|
||||||
## 配置建议
|
|
||||||
|
|
||||||
### 小型部署(< 100 设备)
|
|
||||||
|
|
||||||
```java
|
|
||||||
private final ExecutorService executorService = Executors.newFixedThreadPool(20);
|
|
||||||
.setConnectTimeout(5, TimeUnit.SECONDS)
|
|
||||||
.setResponseTimeout(10, TimeUnit.SECONDS)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 中型部署(100-500 设备)
|
|
||||||
|
|
||||||
```java
|
|
||||||
private final ExecutorService executorService = Executors.newFixedThreadPool(50);
|
|
||||||
.setConnectTimeout(3, TimeUnit.SECONDS)
|
|
||||||
.setResponseTimeout(5, TimeUnit.SECONDS)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 大型部署(> 500 设备)
|
|
||||||
|
|
||||||
```java
|
|
||||||
private final ExecutorService executorService = Executors.newFixedThreadPool(100);
|
|
||||||
.setConnectTimeout(2, TimeUnit.SECONDS)
|
|
||||||
.setResponseTimeout(3, TimeUnit.SECONDS)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
通过以上优化,网络扫描性能提升约 70%,用户体验显著改善。主要改进包括:
|
|
||||||
|
|
||||||
1. ✅ 增加并发线程数(20 -> 50)
|
|
||||||
2. ✅ 使用 CompletableFuture 提高异步效率
|
|
||||||
3. ✅ 添加 HTTP 连接超时配置
|
|
||||||
4. ✅ 优化日志输出和进度统计
|
|
||||||
5. ✅ 改进超时和异常处理
|
|
||||||
6. ✅ 添加扫描时间统计
|
|
||||||
|
|
||||||
建议在生产环境部署前进行充分测试,根据实际情况调整线程数和超时时间。
|
|
||||||
@ -1,243 +0,0 @@
|
|||||||
# 网络扫描超时和进度条修复说明
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
|
|
||||||
1. **进度条显示问题**:后端是同步扫描,前端无法实时获取进度,导致进度条显示不准确
|
|
||||||
2. **前端请求超时**:默认 10 秒超时,大网段扫描时后端还未完成前端就超时了
|
|
||||||
|
|
||||||
## 解决方案
|
|
||||||
|
|
||||||
### 1. 前端修改
|
|
||||||
|
|
||||||
#### 增加请求超时时间
|
|
||||||
- 文件:`src/service/api/device.ts`
|
|
||||||
- 修改:将网络扫描请求的超时时间从默认 10 秒增加到 10 分钟
|
|
||||||
```typescript
|
|
||||||
export function fetchScanNetwork(data: Api.Device.NetworkScanRequest) {
|
|
||||||
return request<Api.Device.NetworkScanResult>({
|
|
||||||
url: '/device/amt/scanNetwork',
|
|
||||||
method: 'post',
|
|
||||||
data,
|
|
||||||
timeout: 10 * 60 * 1000 // 10 分钟超时
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 修复进度条显示
|
|
||||||
- 文件:`src/views/device/list/index.vue`
|
|
||||||
- 修改:使用不确定进度条(processing 状态)替代百分比进度条
|
|
||||||
- 移除:`scanProgress` 计算属性和 `scanProgressTimer` 轮询
|
|
||||||
- 显示:简单的"正在扫描网络,请稍候..."提示
|
|
||||||
|
|
||||||
**修改前**:
|
|
||||||
```vue
|
|
||||||
<n-progress
|
|
||||||
type="line"
|
|
||||||
:percentage="scanProgress"
|
|
||||||
:status="scanning ? 'default' : 'success'"
|
|
||||||
/>
|
|
||||||
<div class="text-sm text-gray-500 mt-2">
|
|
||||||
已扫描: {{ scanResult?.scannedIps || 0 }} / {{ scanResult?.totalIps || 0 }}
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**修改后**:
|
|
||||||
```vue
|
|
||||||
<n-progress
|
|
||||||
type="line"
|
|
||||||
:percentage="100"
|
|
||||||
status="default"
|
|
||||||
:show-indicator="false"
|
|
||||||
processing
|
|
||||||
/>
|
|
||||||
<div class="text-sm text-gray-500 mt-2">
|
|
||||||
正在扫描网络,请稍候...
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 后端修改
|
|
||||||
|
|
||||||
#### 增加扫描超时时间
|
|
||||||
- 文件:`backend/src/main/java/com/soybean/admin/service/AmtNetworkScanService.java`
|
|
||||||
- 修改:将扫描超时从 5 分钟增加到 10 分钟
|
|
||||||
```java
|
|
||||||
// 等待所有扫描完成(最多等待 10 分钟)
|
|
||||||
boolean completed = latch.await(10, TimeUnit.MINUTES);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 增加并发线程数
|
|
||||||
- 从 10 个线程增加到 20 个线程
|
|
||||||
- 提高扫描效率,减少总扫描时间
|
|
||||||
```java
|
|
||||||
private final ExecutorService executorService = Executors.newFixedThreadPool(20);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 优化进度日志
|
|
||||||
- 使用 `AtomicInteger` 线程安全地统计已扫描数量
|
|
||||||
- 每扫描 10 个 IP 输出一次进度日志
|
|
||||||
```java
|
|
||||||
AtomicInteger scannedCount = new AtomicInteger(0);
|
|
||||||
// ...
|
|
||||||
int scanned = scannedCount.incrementAndGet();
|
|
||||||
result.setScannedIps(scanned);
|
|
||||||
if (scanned % 10 == 0) {
|
|
||||||
logger.info("扫描进度: {}/{}", scanned, ipList.size());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 添加超时警告
|
|
||||||
```java
|
|
||||||
boolean completed = latch.await(10, TimeUnit.MINUTES);
|
|
||||||
if (!completed) {
|
|
||||||
logger.warn("扫描超时,部分 IP 未完成扫描");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 性能优化
|
|
||||||
|
|
||||||
### 扫描时间估算
|
|
||||||
|
|
||||||
假设每个 IP 扫描耗时 2 秒(包括连接超时):
|
|
||||||
|
|
||||||
| 网段大小 | IP 数量 | 10 线程耗时 | 20 线程耗时 |
|
|
||||||
|---------|---------|------------|------------|
|
|
||||||
| /28 | 14 | ~3 秒 | ~2 秒 |
|
|
||||||
| /27 | 30 | ~6 秒 | ~3 秒 |
|
|
||||||
| /26 | 62 | ~13 秒 | ~7 秒 |
|
|
||||||
| /25 | 126 | ~26 秒 | ~13 秒 |
|
|
||||||
| /24 | 254 | ~51 秒 | ~26 秒 |
|
|
||||||
|
|
||||||
### 优化建议
|
|
||||||
|
|
||||||
1. **小网段扫描**(/28 - /26):
|
|
||||||
- 推荐使用,扫描时间短
|
|
||||||
- 适合精确定位设备
|
|
||||||
|
|
||||||
2. **中等网段**(/25 - /24):
|
|
||||||
- 可以使用,但需要等待
|
|
||||||
- 建议在非高峰时段扫描
|
|
||||||
|
|
||||||
3. **大网段**(/23 及以上):
|
|
||||||
- 不推荐,扫描时间过长
|
|
||||||
- 建议分段扫描
|
|
||||||
|
|
||||||
## 测试步骤
|
|
||||||
|
|
||||||
### 1. 重新编译后端
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
mvn clean package -DskipTests
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 重启后端服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
java -jar target/soybean-admin-1.0.0.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
或使用批处理文件:
|
|
||||||
```bash
|
|
||||||
start_backend.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 测试扫描功能
|
|
||||||
|
|
||||||
#### 测试场景 1:小网段(快速测试)
|
|
||||||
- 网络地址:`192.168.8.0`
|
|
||||||
- 子网掩码:`/28` 或 `255.255.255.240`
|
|
||||||
- 预期时间:2-5 秒
|
|
||||||
- 预期结果:进度条显示动画,扫描完成后显示结果
|
|
||||||
|
|
||||||
#### 测试场景 2:中等网段
|
|
||||||
- 网络地址:`192.168.8.0`
|
|
||||||
- 子网掩码:`/24` 或 `255.255.255.0`
|
|
||||||
- 预期时间:20-40 秒
|
|
||||||
- 预期结果:进度条持续显示动画,不会超时
|
|
||||||
|
|
||||||
#### 测试场景 3:验证超时处理
|
|
||||||
- 网络地址:`192.168.0.0`
|
|
||||||
- 子网掩码:`/23` 或 `255.255.254.0`
|
|
||||||
- 预期时间:1-2 分钟
|
|
||||||
- 预期结果:能够完成扫描,不会前端超时
|
|
||||||
|
|
||||||
## 后端日志示例
|
|
||||||
|
|
||||||
扫描过程中,后端会输出详细日志:
|
|
||||||
|
|
||||||
```
|
|
||||||
2026-03-01 16:00:00.000 INFO --- AmtNetworkScanService : 开始扫描网络: 192.168.8.0/24
|
|
||||||
2026-03-01 16:00:00.010 INFO --- AmtNetworkScanService : 生成 IP 列表,共 254 个 IP
|
|
||||||
2026-03-01 16:00:02.000 INFO --- AmtNetworkScanService : 扫描进度: 10/254
|
|
||||||
2026-03-01 16:00:04.000 INFO --- AmtNetworkScanService : 扫描进度: 20/254
|
|
||||||
2026-03-01 16:00:06.000 INFO --- AmtNetworkScanService : 扫描进度: 30/254
|
|
||||||
...
|
|
||||||
2026-03-01 16:00:10.000 INFO --- AmtDigestService : 发现 AMT 设备: 192.168.8.112 (DESKTOP-ABC123)
|
|
||||||
...
|
|
||||||
2026-03-01 16:00:30.000 INFO --- AmtNetworkScanService : 扫描完成,发现 3 个设备
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **网络环境**:
|
|
||||||
- 扫描时间受网络延迟影响
|
|
||||||
- 局域网环境扫描更快
|
|
||||||
- 跨网段扫描可能较慢
|
|
||||||
|
|
||||||
2. **AMT 设备响应**:
|
|
||||||
- 已启用 AMT 的设备响应快
|
|
||||||
- 未启用 AMT 的设备会超时(增加扫描时间)
|
|
||||||
|
|
||||||
3. **防火墙设置**:
|
|
||||||
- 确保 AMT 端口(16992/16993)未被防火墙阻止
|
|
||||||
- 某些网络可能限制端口扫描
|
|
||||||
|
|
||||||
4. **资源占用**:
|
|
||||||
- 20 个并发线程会占用一定系统资源
|
|
||||||
- 大网段扫描时注意服务器负载
|
|
||||||
|
|
||||||
## 故障排查
|
|
||||||
|
|
||||||
### 问题 1:前端仍然超时
|
|
||||||
- 检查是否重新编译了前端代码
|
|
||||||
- 清除浏览器缓存
|
|
||||||
- 检查网络请求的实际超时时间
|
|
||||||
|
|
||||||
### 问题 2:后端扫描超时
|
|
||||||
- 检查网段大小,是否过大
|
|
||||||
- 查看后端日志,确认扫描进度
|
|
||||||
- 考虑增加后端超时时间或减小扫描范围
|
|
||||||
|
|
||||||
### 问题 3:进度条不显示
|
|
||||||
- 检查 `scanning` 状态是否正确
|
|
||||||
- 查看浏览器控制台是否有错误
|
|
||||||
- 确认 Naive UI 版本支持 `processing` 属性
|
|
||||||
|
|
||||||
### 问题 4:扫描速度慢
|
|
||||||
- 检查网络延迟
|
|
||||||
- 考虑增加并发线程数(需要评估服务器性能)
|
|
||||||
- 缩小扫描范围
|
|
||||||
|
|
||||||
## 未来优化方向
|
|
||||||
|
|
||||||
1. **实时进度推送**:
|
|
||||||
- 使用 WebSocket 或 SSE 实现实时进度推送
|
|
||||||
- 前端可以显示准确的扫描进度
|
|
||||||
|
|
||||||
2. **分批扫描**:
|
|
||||||
- 将大网段分成多个小批次
|
|
||||||
- 每批扫描完成后返回部分结果
|
|
||||||
|
|
||||||
3. **智能扫描**:
|
|
||||||
- 先 ping 检测主机是否在线
|
|
||||||
- 只对在线主机进行 AMT 检测
|
|
||||||
|
|
||||||
4. **缓存机制**:
|
|
||||||
- 缓存最近扫描的结果
|
|
||||||
- 避免重复扫描同一网段
|
|
||||||
|
|
||||||
5. **后台任务**:
|
|
||||||
- 将扫描作为后台任务执行
|
|
||||||
- 用户可以继续其他操作
|
|
||||||
- 扫描完成后通知用户
|
|
||||||
@ -6,13 +6,10 @@ import com.soybean.admin.dto.PageRequest;
|
|||||||
import com.soybean.admin.dto.PageResponse;
|
import com.soybean.admin.dto.PageResponse;
|
||||||
import com.soybean.admin.dto.AmtTestRequest;
|
import com.soybean.admin.dto.AmtTestRequest;
|
||||||
import com.soybean.admin.dto.AmtDeviceInfo;
|
import com.soybean.admin.dto.AmtDeviceInfo;
|
||||||
import com.soybean.admin.dto.NetworkScanRequest;
|
|
||||||
import com.soybean.admin.dto.NetworkScanResult;
|
|
||||||
import com.soybean.admin.entity.Device;
|
import com.soybean.admin.entity.Device;
|
||||||
import com.soybean.admin.service.AmtMockService;
|
import com.soybean.admin.service.AmtMockService;
|
||||||
import com.soybean.admin.service.DeviceService;
|
import com.soybean.admin.service.DeviceService;
|
||||||
import com.soybean.admin.service.AmtDigestService;
|
import com.soybean.admin.service.AmtDigestService;
|
||||||
import com.soybean.admin.service.AmtNetworkScanService;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -36,9 +33,6 @@ public class DeviceController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private AmtDigestService amtDigestService;
|
private AmtDigestService amtDigestService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AmtNetworkScanService amtNetworkScanService;
|
|
||||||
|
|
||||||
// 是否使用模拟模式(用于测试)
|
// 是否使用模拟模式(用于测试)
|
||||||
private boolean useMockMode = false; // 改为 true 启用模拟模式
|
private boolean useMockMode = false; // 改为 true 启用模拟模式
|
||||||
|
|
||||||
@ -167,17 +161,4 @@ public class DeviceController {
|
|||||||
public Result<Boolean> getMockStatus() {
|
public Result<Boolean> getMockStatus() {
|
||||||
return Result.success(useMockMode);
|
return Result.success(useMockMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/amt/scanNetwork")
|
|
||||||
public Result<NetworkScanResult> scanNetwork(@RequestBody NetworkScanRequest request) {
|
|
||||||
logger.info("收到网络扫描请求,网络: {}/{}", request.getNetworkAddress(), request.getSubnetMask());
|
|
||||||
try {
|
|
||||||
NetworkScanResult result = amtNetworkScanService.scanNetwork(request);
|
|
||||||
logger.info("网络扫描完成,发现 {} 个设备", result.getFoundDevices());
|
|
||||||
return Result.success(result);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("网络扫描失败", e);
|
|
||||||
return Result.error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,5 +7,5 @@ public class AmtTestRequest {
|
|||||||
private String ipAddress;
|
private String ipAddress;
|
||||||
private String username;
|
private String username;
|
||||||
private String password;
|
private String password;
|
||||||
private String credentialId; // 可选:使用已保存的凭证ID(UUID字符串)
|
private Long credentialId; // 可选:使用已保存的凭证ID
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
package com.soybean.admin.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class NetworkScanRequest {
|
|
||||||
private String networkAddress; // 网络号,如 192.168.8.0
|
|
||||||
private String subnetMask; // 子网掩码,如 255.255.255.0 或 /24
|
|
||||||
private String credentialId; // 使用的凭证ID
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
package com.soybean.admin.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class NetworkScanResult {
|
|
||||||
private int totalIps; // 总 IP 数量
|
|
||||||
private int scannedIps; // 已扫描 IP 数量
|
|
||||||
private int foundDevices; // 发现的设备数量
|
|
||||||
private boolean completed; // 是否完成
|
|
||||||
private List<ScannedDevice> devices; // 发现的设备列表
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
package com.soybean.admin.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class ScannedDevice {
|
|
||||||
private String ipAddress; // IP 地址
|
|
||||||
private String deviceName; // 设备名称
|
|
||||||
private String deviceCode; // UUID
|
|
||||||
private String macAddress; // MAC 地址
|
|
||||||
private String status; // 扫描状态:success, failed, scanning
|
|
||||||
private String errorMessage; // 错误信息(如果失败)
|
|
||||||
}
|
|
||||||
@ -46,44 +46,16 @@ public class AmtDigestService {
|
|||||||
String username;
|
String username;
|
||||||
String password;
|
String password;
|
||||||
|
|
||||||
if (request.getCredentialId() != null && !request.getCredentialId().isEmpty()) {
|
if (request.getCredentialId() != null) {
|
||||||
logger.info("使用凭证 ID: {}", request.getCredentialId());
|
AmtCredential credential = amtCredentialService.getCredentialById(request.getCredentialId().toString());
|
||||||
|
|
||||||
AmtCredential credential = amtCredentialService.getCredentialById(request.getCredentialId());
|
|
||||||
if (credential == null) {
|
if (credential == null) {
|
||||||
logger.error("凭证不存在,ID: {}", request.getCredentialId());
|
throw new RuntimeException("凭证不存在");
|
||||||
throw new RuntimeException("凭证不存在,ID: " + request.getCredentialId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("找到凭证: {}, 用户名: {}, 密码长度: {}",
|
|
||||||
credential.getCredentialName(),
|
|
||||||
credential.getUsername(),
|
|
||||||
credential.getPassword() != null ? credential.getPassword().length() : 0);
|
|
||||||
|
|
||||||
username = credential.getUsername();
|
username = credential.getUsername();
|
||||||
password = credential.getPassword();
|
password = credential.getPassword();
|
||||||
|
|
||||||
if (username == null || username.isEmpty()) {
|
|
||||||
logger.error("凭证用户名为空,凭证名称: {}", credential.getCredentialName());
|
|
||||||
throw new RuntimeException("凭证用户名为空");
|
|
||||||
}
|
|
||||||
if (password == null || password.isEmpty()) {
|
|
||||||
logger.error("凭证密码为空,凭证名称: {}", credential.getCredentialName());
|
|
||||||
throw new RuntimeException("凭证密码为空");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
username = request.getUsername();
|
username = request.getUsername();
|
||||||
password = request.getPassword();
|
password = request.getPassword();
|
||||||
logger.info("使用手动输入凭证 - 用户名: {}, 密码长度: {}",
|
|
||||||
username,
|
|
||||||
password != null ? password.length() : 0);
|
|
||||||
|
|
||||||
if (username == null || username.isEmpty()) {
|
|
||||||
throw new RuntimeException("用户名不能为空");
|
|
||||||
}
|
|
||||||
if (password == null || password.isEmpty()) {
|
|
||||||
throw new RuntimeException("密码不能为空");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Digest 认证 - 用户名: {}", username);
|
logger.info("Digest 认证 - 用户名: {}", username);
|
||||||
@ -113,169 +85,42 @@ public class AmtDigestService {
|
|||||||
String username;
|
String username;
|
||||||
String password;
|
String password;
|
||||||
|
|
||||||
if (request.getCredentialId() != null && !request.getCredentialId().isEmpty()) {
|
if (request.getCredentialId() != null) {
|
||||||
logger.info("使用凭证 ID 获取设备信息: {}", request.getCredentialId());
|
AmtCredential credential = amtCredentialService.getCredentialById(request.getCredentialId().toString());
|
||||||
|
|
||||||
AmtCredential credential = amtCredentialService.getCredentialById(request.getCredentialId());
|
|
||||||
if (credential == null) {
|
if (credential == null) {
|
||||||
logger.error("凭证不存在,ID: {}", request.getCredentialId());
|
throw new RuntimeException("凭证不存在");
|
||||||
throw new RuntimeException("凭证不存在,ID: " + request.getCredentialId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("找到凭证: {}, 用户名: {}",
|
|
||||||
credential.getCredentialName(),
|
|
||||||
credential.getUsername());
|
|
||||||
|
|
||||||
username = credential.getUsername();
|
username = credential.getUsername();
|
||||||
password = credential.getPassword();
|
password = credential.getPassword();
|
||||||
|
|
||||||
if (username == null || username.isEmpty()) {
|
|
||||||
logger.error("凭证用户名为空,凭证名称: {}", credential.getCredentialName());
|
|
||||||
throw new RuntimeException("凭证用户名为空");
|
|
||||||
}
|
|
||||||
if (password == null || password.isEmpty()) {
|
|
||||||
logger.error("凭证密码为空,凭证名称: {}", credential.getCredentialName());
|
|
||||||
throw new RuntimeException("凭证密码为空");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
username = request.getUsername();
|
username = request.getUsername();
|
||||||
password = request.getPassword();
|
password = request.getPassword();
|
||||||
|
|
||||||
if (username == null || username.isEmpty()) {
|
|
||||||
throw new RuntimeException("用户名不能为空");
|
|
||||||
}
|
|
||||||
if (password == null || password.isEmpty()) {
|
|
||||||
throw new RuntimeException("密码不能为空");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AmtDeviceInfo deviceInfo = new AmtDeviceInfo();
|
AmtDeviceInfo deviceInfo = new AmtDeviceInfo();
|
||||||
|
|
||||||
// 使用 Identify 请求获取基本信息(这个请求已经验证可以成功)
|
String systemInfo = sendDigestRequest(
|
||||||
String identifyResponse = sendDigestRequest(
|
|
||||||
request.getIpAddress(),
|
request.getIpAddress(),
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
getIdentifyRequest()
|
getSystemInfoRequest()
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info("Identify 响应长度: {} 字节", identifyResponse.length());
|
|
||||||
|
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
factory.setNamespaceAware(true);
|
factory.setNamespaceAware(true);
|
||||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
Document doc = builder.parse(new ByteArrayInputStream(identifyResponse.getBytes()));
|
Document doc = builder.parse(new ByteArrayInputStream(systemInfo.getBytes()));
|
||||||
|
|
||||||
deviceInfo.setIpAddress(request.getIpAddress());
|
deviceInfo.setIpAddress(request.getIpAddress());
|
||||||
|
deviceInfo.setDeviceName(extractValue(doc, "ElementName"));
|
||||||
// 从 Identify 响应中提取信息
|
deviceInfo.setMacAddress("00:00:00:00:00:00"); // 简化处理
|
||||||
String productVendor = extractValue(doc, "ProductVendor");
|
|
||||||
String productVersion = extractValue(doc, "ProductVersion");
|
|
||||||
|
|
||||||
// 生成设备名称
|
|
||||||
String deviceName = "AMT-" + request.getIpAddress().replace(".", "-");
|
|
||||||
if (productVendor != null && !productVendor.equals("Unknown")) {
|
|
||||||
deviceName = productVendor + "-" + request.getIpAddress().replace(".", "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceInfo.setDeviceName(deviceName);
|
|
||||||
deviceInfo.setDeviceCode(UUID.randomUUID().toString());
|
deviceInfo.setDeviceCode(UUID.randomUUID().toString());
|
||||||
|
|
||||||
// 尝试获取 MAC 地址
|
|
||||||
String macAddress = getMacAddress(request.getIpAddress(), username, password);
|
|
||||||
deviceInfo.setMacAddress(macAddress);
|
|
||||||
|
|
||||||
logger.info("成功解析设备信息 - 名称: {}, UUID: {}, MAC: {}, 厂商: {}, 版本: {}",
|
|
||||||
deviceInfo.getDeviceName(), deviceInfo.getDeviceCode(), macAddress, productVendor, productVersion);
|
|
||||||
|
|
||||||
return deviceInfo;
|
return deviceInfo;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("获取 AMT 设备信息失败", e);
|
|
||||||
throw new RuntimeException("获取 AMT 设备信息失败: " + e.getMessage());
|
throw new RuntimeException("获取 AMT 设备信息失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 MAC 地址
|
|
||||||
*/
|
|
||||||
private String getMacAddress(String ipAddress, String username, String password) {
|
|
||||||
try {
|
|
||||||
String enumerateResponse = sendDigestRequest(
|
|
||||||
ipAddress,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
getEnumerateNetworkRequest()
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("网络接口枚举响应长度: {} 字节", enumerateResponse.length());
|
|
||||||
|
|
||||||
// 解析枚举响应获取 EnumerationContext
|
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
|
||||||
factory.setNamespaceAware(true);
|
|
||||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
|
||||||
Document doc = builder.parse(new ByteArrayInputStream(enumerateResponse.getBytes()));
|
|
||||||
|
|
||||||
String enumerationContext = extractValue(doc, "EnumerationContext");
|
|
||||||
if (enumerationContext == null || enumerationContext.equals("Unknown")) {
|
|
||||||
logger.warn("无法获取 EnumerationContext,使用默认 MAC 地址");
|
|
||||||
return "00:00:00:00:00:00";
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("EnumerationContext: {}", enumerationContext);
|
|
||||||
|
|
||||||
// 使用 EnumerationContext 拉取数据
|
|
||||||
String pullResponse = sendDigestRequest(
|
|
||||||
ipAddress,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
getPullNetworkRequest(enumerationContext)
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("网络接口拉取响应长度: {} 字节", pullResponse.length());
|
|
||||||
|
|
||||||
// 解析 MAC 地址
|
|
||||||
Document pullDoc = builder.parse(new ByteArrayInputStream(pullResponse.getBytes()));
|
|
||||||
String macAddress = extractValue(pullDoc, "MACAddress");
|
|
||||||
|
|
||||||
if (macAddress != null && !macAddress.equals("Unknown")) {
|
|
||||||
// 格式化 MAC 地址(添加冒号分隔符)
|
|
||||||
macAddress = formatMacAddress(macAddress);
|
|
||||||
logger.info("成功获取 MAC 地址: {}", macAddress);
|
|
||||||
return macAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn("无法从响应中提取 MAC 地址");
|
|
||||||
return "00:00:00:00:00:00";
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("获取 MAC 地址失败: {}", e.getMessage());
|
|
||||||
return "00:00:00:00:00:00";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化 MAC 地址
|
|
||||||
*/
|
|
||||||
private String formatMacAddress(String mac) {
|
|
||||||
// 移除所有非十六进制字符
|
|
||||||
mac = mac.replaceAll("[^0-9A-Fa-f]", "");
|
|
||||||
|
|
||||||
// 如果长度不是 12,返回原值
|
|
||||||
if (mac.length() != 12) {
|
|
||||||
return mac;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每两个字符添加一个冒号
|
|
||||||
StringBuilder formatted = new StringBuilder();
|
|
||||||
for (int i = 0; i < mac.length(); i += 2) {
|
|
||||||
if (i > 0) {
|
|
||||||
formatted.append(":");
|
|
||||||
}
|
|
||||||
formatted.append(mac.substring(i, i + 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted.toString().toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送 Digest 认证请求
|
* 发送 Digest 认证请求
|
||||||
@ -302,15 +147,8 @@ public class AmtDigestService {
|
|||||||
new UsernamePasswordCredentials(username, password.toCharArray())
|
new UsernamePasswordCredentials(username, password.toCharArray())
|
||||||
);
|
);
|
||||||
|
|
||||||
// 配置超时时间:连接超时 3 秒,响应超时 5 秒
|
|
||||||
org.apache.hc.client5.http.config.RequestConfig requestConfig = org.apache.hc.client5.http.config.RequestConfig.custom()
|
|
||||||
.setConnectTimeout(3, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.setResponseTimeout(5, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try (CloseableHttpClient httpClient = HttpClients.custom()
|
try (CloseableHttpClient httpClient = HttpClients.custom()
|
||||||
.setDefaultCredentialsProvider(credsProvider)
|
.setDefaultCredentialsProvider(credsProvider)
|
||||||
.setDefaultRequestConfig(requestConfig)
|
|
||||||
.build()) {
|
.build()) {
|
||||||
|
|
||||||
HttpHost target = new HttpHost(protocol, ipAddress, port);
|
HttpHost target = new HttpHost(protocol, ipAddress, port);
|
||||||
@ -347,56 +185,22 @@ public class AmtDigestService {
|
|||||||
"</s:Body>" +
|
"</s:Body>" +
|
||||||
"</s:Envelope>";
|
"</s:Envelope>";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String getSystemInfoRequest() {
|
||||||
* 枚举网络接口请求
|
|
||||||
*/
|
|
||||||
private String getEnumerateNetworkRequest() {
|
|
||||||
String messageId = UUID.randomUUID().toString();
|
|
||||||
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||||
"<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" " +
|
"<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" " +
|
||||||
"xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" " +
|
"xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" " +
|
||||||
"xmlns:wsman=\"http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd\" " +
|
"xmlns:wsman=\"http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd\">" +
|
||||||
"xmlns:wsen=\"http://schemas.xmlsoap.org/ws/2004/09/enumeration\">" +
|
|
||||||
"<s:Header>" +
|
"<s:Header>" +
|
||||||
"<wsa:Action s:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate</wsa:Action>" +
|
"<wsa:Action s:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2004/09/transfer/Get</wsa:Action>" +
|
||||||
"<wsa:To s:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>" +
|
"<wsa:To s:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>" +
|
||||||
"<wsman:ResourceURI s:mustUnderstand=\"true\">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_EthernetPort</wsman:ResourceURI>" +
|
"<wsman:ResourceURI s:mustUnderstand=\"true\">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem</wsman:ResourceURI>" +
|
||||||
"<wsa:MessageID s:mustUnderstand=\"true\">uuid:" + messageId + "</wsa:MessageID>" +
|
"<wsa:MessageID s:mustUnderstand=\"true\">uuid:" + UUID.randomUUID().toString() + "</wsa:MessageID>" +
|
||||||
"<wsa:ReplyTo>" +
|
"<wsa:ReplyTo>" +
|
||||||
"<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>" +
|
"<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>" +
|
||||||
"</wsa:ReplyTo>" +
|
"</wsa:ReplyTo>" +
|
||||||
"</s:Header>" +
|
"</s:Header>" +
|
||||||
"<s:Body>" +
|
"<s:Body/>" +
|
||||||
"<wsen:Enumerate/>" +
|
|
||||||
"</s:Body>" +
|
|
||||||
"</s:Envelope>";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拉取网络接口数据请求
|
|
||||||
*/
|
|
||||||
private String getPullNetworkRequest(String enumerationContext) {
|
|
||||||
String messageId = UUID.randomUUID().toString();
|
|
||||||
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
|
||||||
"<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" " +
|
|
||||||
"xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" " +
|
|
||||||
"xmlns:wsman=\"http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd\" " +
|
|
||||||
"xmlns:wsen=\"http://schemas.xmlsoap.org/ws/2004/09/enumeration\">" +
|
|
||||||
"<s:Header>" +
|
|
||||||
"<wsa:Action s:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull</wsa:Action>" +
|
|
||||||
"<wsa:To s:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>" +
|
|
||||||
"<wsman:ResourceURI s:mustUnderstand=\"true\">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_EthernetPort</wsman:ResourceURI>" +
|
|
||||||
"<wsa:MessageID s:mustUnderstand=\"true\">uuid:" + messageId + "</wsa:MessageID>" +
|
|
||||||
"<wsa:ReplyTo>" +
|
|
||||||
"<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>" +
|
|
||||||
"</wsa:ReplyTo>" +
|
|
||||||
"</s:Header>" +
|
|
||||||
"<s:Body>" +
|
|
||||||
"<wsen:Pull>" +
|
|
||||||
"<wsen:EnumerationContext>" + enumerationContext + "</wsen:EnumerationContext>" +
|
|
||||||
"</wsen:Pull>" +
|
|
||||||
"</s:Body>" +
|
|
||||||
"</s:Envelope>";
|
"</s:Envelope>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,234 +0,0 @@
|
|||||||
package com.soybean.admin.service;
|
|
||||||
|
|
||||||
import com.soybean.admin.dto.*;
|
|
||||||
import com.soybean.admin.entity.AmtCredential;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AMT 网络扫描服务 - 优化版
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class AmtNetworkScanService {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AmtNetworkScanService.class);
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AmtDigestService amtDigestService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AmtCredentialService amtCredentialService;
|
|
||||||
|
|
||||||
// 线程池,用于并发扫描 - 增加到 50 个线程
|
|
||||||
private final ExecutorService executorService = Executors.newFixedThreadPool(50);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫描网络段 - 优化版
|
|
||||||
*/
|
|
||||||
public NetworkScanResult scanNetwork(NetworkScanRequest request) {
|
|
||||||
logger.info("开始扫描网络: {}/{}", request.getNetworkAddress(), request.getSubnetMask());
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
NetworkScanResult result = new NetworkScanResult();
|
|
||||||
List<ScannedDevice> devices = new CopyOnWriteArrayList<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取凭证
|
|
||||||
AmtCredential credential = amtCredentialService.getCredentialById(request.getCredentialId());
|
|
||||||
if (credential == null) {
|
|
||||||
throw new RuntimeException("凭证不存在,ID: " + request.getCredentialId());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成 IP 列表
|
|
||||||
List<String> ipList = generateIpList(request.getNetworkAddress(), request.getSubnetMask());
|
|
||||||
result.setTotalIps(ipList.size());
|
|
||||||
result.setScannedIps(0);
|
|
||||||
result.setFoundDevices(0);
|
|
||||||
result.setCompleted(false);
|
|
||||||
|
|
||||||
logger.info("生成 IP 列表,共 {} 个 IP,使用 50 个线程并发扫描", ipList.size());
|
|
||||||
|
|
||||||
// 使用 CompletableFuture 进行异步扫描
|
|
||||||
AtomicInteger scannedCount = new AtomicInteger(0);
|
|
||||||
AtomicInteger foundCount = new AtomicInteger(0);
|
|
||||||
|
|
||||||
List<CompletableFuture<ScannedDevice>> futures = ipList.stream()
|
|
||||||
.map(ip -> CompletableFuture.supplyAsync(() -> {
|
|
||||||
try {
|
|
||||||
ScannedDevice device = scanSingleDevice(ip, credential);
|
|
||||||
|
|
||||||
int scanned = scannedCount.incrementAndGet();
|
|
||||||
result.setScannedIps(scanned);
|
|
||||||
|
|
||||||
if (device != null && "success".equals(device.getStatus())) {
|
|
||||||
devices.add(device);
|
|
||||||
int foundDevices = foundCount.incrementAndGet();
|
|
||||||
result.setFoundDevices(foundDevices);
|
|
||||||
logger.info("发现 AMT 设备 [{}/{}]: {} ({})", foundDevices, scanned, ip, device.getDeviceName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每扫描 20 个 IP 输出一次进度
|
|
||||||
if (scanned % 20 == 0) {
|
|
||||||
logger.info("扫描进度: {}/{} (已发现 {} 个设备)", scanned, ipList.size(), foundCount.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
return device;
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.debug("扫描 IP {} 失败: {}", ip, e.getMessage());
|
|
||||||
scannedCount.incrementAndGet();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, executorService))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 等待所有任务完成(最多等待 10 分钟)
|
|
||||||
try {
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
|
||||||
.get(10, TimeUnit.MINUTES);
|
|
||||||
logger.info("所有扫描任务完成");
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
logger.warn("扫描超时,部分 IP 未完成扫描");
|
|
||||||
// 取消未完成的任务
|
|
||||||
futures.forEach(f -> f.cancel(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
result.setCompleted(true);
|
|
||||||
result.setDevices(new ArrayList<>(devices));
|
|
||||||
|
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
|
||||||
logger.info("扫描完成,共扫描 {} 个 IP,发现 {} 个设备,耗时 {} 秒",
|
|
||||||
result.getScannedIps(), devices.size(), duration / 1000.0);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("网络扫描失败", e);
|
|
||||||
throw new RuntimeException("网络扫描失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫描单个设备 - 优化版
|
|
||||||
*/
|
|
||||||
private ScannedDevice scanSingleDevice(String ipAddress, AmtCredential credential) {
|
|
||||||
ScannedDevice device = new ScannedDevice();
|
|
||||||
device.setIpAddress(ipAddress);
|
|
||||||
device.setStatus("scanning");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建测试请求
|
|
||||||
AmtTestRequest testRequest = new AmtTestRequest();
|
|
||||||
testRequest.setIpAddress(ipAddress);
|
|
||||||
testRequest.setCredentialId(credential.getCredentialId());
|
|
||||||
|
|
||||||
// 测试连接(使用较短的超时时间)
|
|
||||||
boolean connected = amtDigestService.testAmtConnectionWithDigest(testRequest);
|
|
||||||
|
|
||||||
if (connected) {
|
|
||||||
// 获取设备信息
|
|
||||||
AmtDeviceInfo deviceInfo = amtDigestService.getAmtDeviceInfoWithDigest(testRequest);
|
|
||||||
|
|
||||||
device.setDeviceName(deviceInfo.getDeviceName());
|
|
||||||
device.setDeviceCode(deviceInfo.getDeviceCode());
|
|
||||||
device.setMacAddress(deviceInfo.getMacAddress());
|
|
||||||
device.setStatus("success");
|
|
||||||
} else {
|
|
||||||
device.setStatus("failed");
|
|
||||||
device.setErrorMessage("连接失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
device.setStatus("failed");
|
|
||||||
device.setErrorMessage(e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return device;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成 IP 列表
|
|
||||||
*/
|
|
||||||
private List<String> generateIpList(String networkAddress, String subnetMask) {
|
|
||||||
List<String> ipList = new ArrayList<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 解析网络地址
|
|
||||||
String[] networkParts = networkAddress.split("\\.");
|
|
||||||
int[] network = new int[4];
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
network[i] = Integer.parseInt(networkParts[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析子网掩码
|
|
||||||
int[] mask = parseSubnetMask(subnetMask);
|
|
||||||
|
|
||||||
// 计算网络范围
|
|
||||||
int[] start = new int[4];
|
|
||||||
int[] end = new int[4];
|
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
start[i] = network[i] & mask[i];
|
|
||||||
end[i] = start[i] | (~mask[i] & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成 IP 列表(跳过网络地址和广播地址)
|
|
||||||
for (int a = start[0]; a <= end[0]; a++) {
|
|
||||||
for (int b = start[1]; b <= end[1]; b++) {
|
|
||||||
for (int c = start[2]; c <= end[2]; c++) {
|
|
||||||
for (int d = start[3]; d <= end[3]; d++) {
|
|
||||||
// 跳过网络地址和广播地址
|
|
||||||
if ((a == start[0] && b == start[1] && c == start[2] && d == start[3]) ||
|
|
||||||
(a == end[0] && b == end[1] && c == end[2] && d == end[3])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ipList.add(String.format("%d.%d.%d.%d", a, b, c, d));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("生成 IP 列表失败", e);
|
|
||||||
throw new RuntimeException("生成 IP 列表失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析子网掩码
|
|
||||||
*/
|
|
||||||
private int[] parseSubnetMask(String subnetMask) {
|
|
||||||
int[] mask = new int[4];
|
|
||||||
|
|
||||||
if (subnetMask.startsWith("/")) {
|
|
||||||
// CIDR 格式,如 /24
|
|
||||||
int cidr = Integer.parseInt(subnetMask.substring(1));
|
|
||||||
long maskValue = (0xFFFFFFFFL << (32 - cidr)) & 0xFFFFFFFFL;
|
|
||||||
|
|
||||||
mask[0] = (int)((maskValue >> 24) & 0xFF);
|
|
||||||
mask[1] = (int)((maskValue >> 16) & 0xFF);
|
|
||||||
mask[2] = (int)((maskValue >> 8) & 0xFF);
|
|
||||||
mask[3] = (int)(maskValue & 0xFF);
|
|
||||||
} else {
|
|
||||||
// 点分十进制格式,如 255.255.255.0
|
|
||||||
String[] parts = subnetMask.split("\\.");
|
|
||||||
if (parts.length != 4) {
|
|
||||||
throw new RuntimeException("子网掩码格式错误,应为 255.255.255.0 或 /24 格式");
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
mask[i] = Integer.parseInt(parts[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
@echo off
|
|
||||||
echo ========================================
|
|
||||||
echo 重新编译后端并查看日志
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd backend
|
|
||||||
|
|
||||||
echo 清理旧的编译文件...
|
|
||||||
call mvn clean
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo 重新编译项目...
|
|
||||||
call mvn compile
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo 打包项目...
|
|
||||||
call mvn package -DskipTests
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo 编译完成!
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo 现在启动后端服务并查看日志:
|
|
||||||
echo cd backend
|
|
||||||
echo java -jar target/soybean-admin-0.0.1-SNAPSHOT.jar
|
|
||||||
echo.
|
|
||||||
echo 测试时请注意查看控制台输出的日志信息
|
|
||||||
echo 特别关注以下内容:
|
|
||||||
echo - 使用凭证 ID: xxx
|
|
||||||
echo - 查询凭证,ID字符串: xxx
|
|
||||||
echo - 找到凭证: xxx, 用户名: xxx
|
|
||||||
echo.
|
|
||||||
|
|
||||||
pause
|
|
||||||
@ -97,15 +97,3 @@ export function fetchAmtDeviceInfo(data: Api.Device.AmtTestRequest) {
|
|||||||
data
|
data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫描网络段
|
|
||||||
*/
|
|
||||||
export function fetchScanNetwork(data: Api.Device.NetworkScanRequest) {
|
|
||||||
return request<Api.Device.NetworkScanResult>({
|
|
||||||
url: '/device/amt/scanNetwork',
|
|
||||||
method: 'post',
|
|
||||||
data,
|
|
||||||
timeout: 10 * 60 * 1000 // 10 分钟超时
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
44
src/typings/api/device.d.ts
vendored
44
src/typings/api/device.d.ts
vendored
@ -60,8 +60,8 @@ declare namespace Api {
|
|||||||
username?: string;
|
username?: string;
|
||||||
/** 密码 */
|
/** 密码 */
|
||||||
password?: string;
|
password?: string;
|
||||||
/** 凭证ID(UUID字符串) */
|
/** 凭证ID */
|
||||||
credentialId?: string | number;
|
credentialId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** AMT 设备信息 */
|
/** AMT 设备信息 */
|
||||||
@ -75,45 +75,5 @@ declare namespace Api {
|
|||||||
/** MAC地址 */
|
/** MAC地址 */
|
||||||
macAddress: string;
|
macAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 网络扫描请求 */
|
|
||||||
interface NetworkScanRequest {
|
|
||||||
/** 网络地址 */
|
|
||||||
networkAddress: string;
|
|
||||||
/** 子网掩码 */
|
|
||||||
subnetMask: string;
|
|
||||||
/** 凭证ID */
|
|
||||||
credentialId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 扫描到的设备 */
|
|
||||||
interface ScannedDevice {
|
|
||||||
/** IP地址 */
|
|
||||||
ipAddress: string;
|
|
||||||
/** 设备名称 */
|
|
||||||
deviceName: string;
|
|
||||||
/** 设备编号 */
|
|
||||||
deviceCode: string;
|
|
||||||
/** MAC地址 */
|
|
||||||
macAddress: string;
|
|
||||||
/** 扫描状态 */
|
|
||||||
status: 'success' | 'failed' | 'scanning';
|
|
||||||
/** 错误信息 */
|
|
||||||
errorMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 网络扫描结果 */
|
|
||||||
interface NetworkScanResult {
|
|
||||||
/** 总IP数量 */
|
|
||||||
totalIps: number;
|
|
||||||
/** 已扫描IP数量 */
|
|
||||||
scannedIps: number;
|
|
||||||
/** 发现的设备数量 */
|
|
||||||
foundDevices: number;
|
|
||||||
/** 是否完成 */
|
|
||||||
completed: boolean;
|
|
||||||
/** 发现的设备列表 */
|
|
||||||
devices: ScannedDevice[];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@ -59,7 +59,6 @@ declare module 'vue' {
|
|||||||
IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
||||||
IconMdiPower: typeof import('~icons/mdi/power')['default']
|
IconMdiPower: typeof import('~icons/mdi/power')['default']
|
||||||
IconMdiPowerOff: typeof import('~icons/mdi/power-off')['default']
|
IconMdiPowerOff: typeof import('~icons/mdi/power-off')['default']
|
||||||
IconMdiRadar: typeof import('~icons/mdi/radar')['default']
|
|
||||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||||
IconMdiRefreshAuto: typeof import('~icons/mdi/refresh-auto')['default']
|
IconMdiRefreshAuto: typeof import('~icons/mdi/refresh-auto')['default']
|
||||||
IconMdiRefreshOff: typeof import('~icons/mdi/refresh-off')['default']
|
IconMdiRefreshOff: typeof import('~icons/mdi/refresh-off')['default']
|
||||||
@ -200,7 +199,6 @@ declare global {
|
|||||||
const IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
const IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
||||||
const IconMdiPower: typeof import('~icons/mdi/power')['default']
|
const IconMdiPower: typeof import('~icons/mdi/power')['default']
|
||||||
const IconMdiPowerOff: typeof import('~icons/mdi/power-off')['default']
|
const IconMdiPowerOff: typeof import('~icons/mdi/power-off')['default']
|
||||||
const IconMdiRadar: typeof import('~icons/mdi/radar')['default']
|
|
||||||
const IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
const IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||||
const IconMdiRefreshAuto: typeof import('~icons/mdi/refresh-auto')['default']
|
const IconMdiRefreshAuto: typeof import('~icons/mdi/refresh-auto')['default']
|
||||||
const IconMdiRefreshOff: typeof import('~icons/mdi/refresh-off')['default']
|
const IconMdiRefreshOff: typeof import('~icons/mdi/refresh-off')['default']
|
||||||
|
|||||||
@ -98,153 +98,13 @@
|
|||||||
<!-- 添加方式选择(仅新增时显示) -->
|
<!-- 添加方式选择(仅新增时显示) -->
|
||||||
<n-radio-group v-if="!isEdit" v-model:value="addMode" class="mb-16px">
|
<n-radio-group v-if="!isEdit" v-model:value="addMode" class="mb-16px">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-radio value="manual">AMT 自动添加</n-radio>
|
<n-radio value="manual">手动添加</n-radio>
|
||||||
<n-radio value="amt">手动添加</n-radio>
|
<n-radio value="amt">AMT 自动添加</n-radio>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
|
|
||||||
<!-- AMT 自动添加(网络扫描) -->
|
<!-- AMT 添加方式 -->
|
||||||
<div v-if="!isEdit && addMode === 'manual'" class="mb-16px">
|
<div v-if="!isEdit && addMode === 'amt'" class="mb-16px">
|
||||||
<n-grid :cols="2" :x-gap="16">
|
|
||||||
<!-- 左侧:扫描配置 -->
|
|
||||||
<n-gi>
|
|
||||||
<n-card title="网络扫描配置" size="small" :bordered="false" class="bg-gray-50">
|
|
||||||
<n-form label-placement="left" label-width="100px">
|
|
||||||
<n-form-item label="网络地址" required>
|
|
||||||
<n-input
|
|
||||||
v-model:value="scanFormData.networkAddress"
|
|
||||||
placeholder="例如: 192.168.8.0"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="子网掩码" required>
|
|
||||||
<n-input
|
|
||||||
v-model:value="scanFormData.subnetMask"
|
|
||||||
placeholder="例如: 255.255.255.0 或 /24"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="使用凭证" required>
|
|
||||||
<n-select
|
|
||||||
v-model:value="scanFormData.credentialId"
|
|
||||||
:options="amtCredentials.map(c => ({ label: c.credentialName, value: c.credentialId }))"
|
|
||||||
placeholder="请选择 AMT 凭证"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item>
|
|
||||||
<n-space>
|
|
||||||
<n-button
|
|
||||||
type="primary"
|
|
||||||
:loading="scanning"
|
|
||||||
@click="handleStartScan"
|
|
||||||
>
|
|
||||||
<icon-mdi-radar class="mr-4px" />
|
|
||||||
开始扫描
|
|
||||||
</n-button>
|
|
||||||
<n-tag v-if="scanResult && scanResult.completed" type="success">
|
|
||||||
<icon-mdi-check-circle class="mr-4px" />
|
|
||||||
扫描完成
|
|
||||||
</n-tag>
|
|
||||||
</n-space>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<!-- 扫描进度 -->
|
|
||||||
<n-form-item v-if="scanning" label="扫描进度">
|
|
||||||
<div class="w-full">
|
|
||||||
<n-progress
|
|
||||||
type="line"
|
|
||||||
:percentage="100"
|
|
||||||
status="default"
|
|
||||||
:show-indicator="false"
|
|
||||||
processing
|
|
||||||
/>
|
|
||||||
<div class="text-sm text-gray-500 mt-2">
|
|
||||||
正在扫描网络,请稍候...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</n-gi>
|
|
||||||
|
|
||||||
<!-- 右侧:扫描结果列表 -->
|
|
||||||
<n-gi>
|
|
||||||
<n-card title="扫描结果" size="small" :bordered="false" class="bg-gray-50">
|
|
||||||
<div v-if="!scanResult || scanResult.devices.length === 0" class="text-center py-40px text-gray-400">
|
|
||||||
<icon-mdi-radar class="text-48px mb-8px" />
|
|
||||||
<div>暂无扫描结果</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="max-h-400px overflow-y-auto">
|
|
||||||
<n-list hoverable clickable>
|
|
||||||
<n-list-item
|
|
||||||
v-for="device in scanResult.devices"
|
|
||||||
:key="device.ipAddress"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<n-checkbox
|
|
||||||
:checked="selectedScanDevices.includes(device.ipAddress)"
|
|
||||||
@update:checked="(checked) => handleScanDeviceToggle(device.ipAddress, checked)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<n-thing>
|
|
||||||
<template #header>
|
|
||||||
<n-space align="center">
|
|
||||||
<span class="font-semibold">{{ device.deviceName }}</span>
|
|
||||||
<n-tag
|
|
||||||
:type="device.status === 'success' ? 'success' : 'error'"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{{ device.status === 'success' ? '成功' : '失败' }}
|
|
||||||
</n-tag>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
<template #description>
|
|
||||||
<n-space vertical :size="4">
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="text-gray-500">IP:</span>
|
|
||||||
<span class="ml-4px">{{ device.ipAddress }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="text-gray-500">UUID:</span>
|
|
||||||
<span class="ml-4px">{{ device.deviceCode }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="text-gray-500">MAC:</span>
|
|
||||||
<span class="ml-4px">{{ device.macAddress }}</span>
|
|
||||||
</div>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-thing>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
|
|
||||||
<n-divider class="!my-12px" />
|
|
||||||
|
|
||||||
<n-space justify="space-between" align="center">
|
|
||||||
<div class="text-sm text-gray-500">
|
|
||||||
已选择 {{ selectedScanDevices.length }} / {{ scanResult.devices.length }} 个设备
|
|
||||||
</div>
|
|
||||||
<n-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
:disabled="selectedScanDevices.length === 0"
|
|
||||||
@click="handleBatchAddScannedDevices"
|
|
||||||
>
|
|
||||||
<icon-mdi-plus class="mr-4px" />
|
|
||||||
批量添加
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</n-card>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 手动添加方式 -->
|
|
||||||
<div v-if="!isEdit && addMode === 'amt'">
|
|
||||||
<n-card title="AMT 设备发现" size="small" :bordered="false" class="bg-gray-50">
|
<n-card title="AMT 设备发现" size="small" :bordered="false" class="bg-gray-50">
|
||||||
<n-form label-placement="left" label-width="100px">
|
<n-form label-placement="left" label-width="100px">
|
||||||
<n-form-item label="IP 地址" required>
|
<n-form-item label="IP 地址" required>
|
||||||
@ -312,59 +172,10 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-divider class="!my-16px">设备信息</n-divider>
|
<n-divider class="!my-16px">设备信息</n-divider>
|
||||||
|
|
||||||
<!-- 设备信息表单 -->
|
|
||||||
<n-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="formData"
|
|
||||||
:rules="formRules"
|
|
||||||
label-placement="left"
|
|
||||||
label-width="100px"
|
|
||||||
>
|
|
||||||
<n-grid :cols="2" :x-gap="24">
|
|
||||||
<n-form-item-gi label="设备名称" path="deviceName">
|
|
||||||
<n-input
|
|
||||||
v-model:value="formData.deviceName"
|
|
||||||
placeholder="请输入设备名称"
|
|
||||||
:disabled="!amtTestSuccess"
|
|
||||||
/>
|
|
||||||
</n-form-item-gi>
|
|
||||||
<n-form-item-gi label="UUID" path="deviceCode">
|
|
||||||
<n-input
|
|
||||||
v-model:value="formData.deviceCode"
|
|
||||||
placeholder="请输入UUID"
|
|
||||||
:disabled="!amtTestSuccess"
|
|
||||||
/>
|
|
||||||
</n-form-item-gi>
|
|
||||||
<n-form-item-gi label="IP地址" path="ipAddress">
|
|
||||||
<n-input
|
|
||||||
v-model:value="formData.ipAddress"
|
|
||||||
placeholder="请输入IP地址"
|
|
||||||
:disabled="!amtTestSuccess"
|
|
||||||
/>
|
|
||||||
</n-form-item-gi>
|
|
||||||
<n-form-item-gi label="MAC地址" path="macAddress">
|
|
||||||
<n-input
|
|
||||||
v-model:value="formData.macAddress"
|
|
||||||
placeholder="请输入MAC地址"
|
|
||||||
:disabled="!amtTestSuccess"
|
|
||||||
/>
|
|
||||||
</n-form-item-gi>
|
|
||||||
</n-grid>
|
|
||||||
<n-form-item label="备注" path="remark">
|
|
||||||
<n-input
|
|
||||||
v-model:value="formData.remark"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="请输入备注信息"
|
|
||||||
:rows="3"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编辑模式的设备信息表单 -->
|
<!-- 设备信息表单 -->
|
||||||
<n-form
|
<n-form
|
||||||
v-if="isEdit"
|
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
:rules="formRules"
|
:rules="formRules"
|
||||||
@ -376,25 +187,28 @@
|
|||||||
<n-input
|
<n-input
|
||||||
v-model:value="formData.deviceName"
|
v-model:value="formData.deviceName"
|
||||||
placeholder="请输入设备名称"
|
placeholder="请输入设备名称"
|
||||||
|
:disabled="!isEdit && addMode === 'amt' && !amtTestSuccess"
|
||||||
/>
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="UUID" path="deviceCode">
|
<n-form-item-gi label="UUID" path="deviceCode">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="formData.deviceCode"
|
v-model:value="formData.deviceCode"
|
||||||
placeholder="请输入UUID"
|
placeholder="请输入UUID"
|
||||||
disabled
|
:disabled="isEdit || (addMode === 'amt' && !amtTestSuccess)"
|
||||||
/>
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="IP地址" path="ipAddress">
|
<n-form-item-gi label="IP地址" path="ipAddress">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="formData.ipAddress"
|
v-model:value="formData.ipAddress"
|
||||||
placeholder="请输入IP地址"
|
placeholder="请输入IP地址"
|
||||||
|
:disabled="!isEdit && addMode === 'amt' && !amtTestSuccess"
|
||||||
/>
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="MAC地址" path="macAddress">
|
<n-form-item-gi label="MAC地址" path="macAddress">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="formData.macAddress"
|
v-model:value="formData.macAddress"
|
||||||
placeholder="请输入MAC地址"
|
placeholder="请输入MAC地址"
|
||||||
|
:disabled="!isEdit && addMode === 'amt' && !amtTestSuccess"
|
||||||
/>
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="设备状态" path="status">
|
<n-form-item-gi label="设备状态" path="status">
|
||||||
@ -418,26 +232,10 @@
|
|||||||
<n-space justify="end">
|
<n-space justify="end">
|
||||||
<n-button @click="modalVisible = false">取消</n-button>
|
<n-button @click="modalVisible = false">取消</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="!isEdit && addMode === 'amt'"
|
|
||||||
type="primary"
|
|
||||||
@click="handleSubmit"
|
|
||||||
:loading="submitLoading"
|
|
||||||
:disabled="!amtTestSuccess"
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</n-button>
|
|
||||||
<n-button
|
|
||||||
v-else-if="!isEdit && addMode === 'manual'"
|
|
||||||
type="primary"
|
|
||||||
@click="modalVisible = false"
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</n-button>
|
|
||||||
<n-button
|
|
||||||
v-else
|
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
:loading="submitLoading"
|
:loading="submitLoading"
|
||||||
|
:disabled="!isEdit && addMode === 'amt' && !amtTestSuccess"
|
||||||
>
|
>
|
||||||
确定
|
确定
|
||||||
</n-button>
|
</n-button>
|
||||||
@ -471,9 +269,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, h, onMounted, computed } from 'vue';
|
import { ref, reactive, h, onMounted } from 'vue';
|
||||||
import type { DataTableColumns } from 'naive-ui';
|
import type { DataTableColumns } from 'naive-ui';
|
||||||
import { NButton, NSpace, NTag, NPopconfirm, NCheckbox, NList, NListItem, NThing } from 'naive-ui';
|
import { NButton, NSpace, NTag, NPopconfirm } from 'naive-ui';
|
||||||
import {
|
import {
|
||||||
fetchDeviceList,
|
fetchDeviceList,
|
||||||
fetchCreateDevice,
|
fetchCreateDevice,
|
||||||
@ -481,8 +279,7 @@ import {
|
|||||||
fetchDeleteDevice,
|
fetchDeleteDevice,
|
||||||
fetchBatchDeleteDevice,
|
fetchBatchDeleteDevice,
|
||||||
fetchTestAmtConnection,
|
fetchTestAmtConnection,
|
||||||
fetchAmtDeviceInfo,
|
fetchAmtDeviceInfo
|
||||||
fetchScanNetwork
|
|
||||||
} from '@/service/api/device';
|
} from '@/service/api/device';
|
||||||
import { fetchAllActiveCredentials, type AmtCredential } from '@/service/api/amt';
|
import { fetchAllActiveCredentials, type AmtCredential } from '@/service/api/amt';
|
||||||
|
|
||||||
@ -658,23 +455,13 @@ const amtFormData = reactive({
|
|||||||
ipAddress: '',
|
ipAddress: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
credentialId: null as string | null,
|
credentialId: null as number | null,
|
||||||
useCredential: false
|
useCredential: false
|
||||||
});
|
});
|
||||||
const amtCredentials = ref<AmtCredential[]>([]);
|
const amtCredentials = ref<AmtCredential[]>([]);
|
||||||
const amtTesting = ref(false);
|
const amtTesting = ref(false);
|
||||||
const amtTestSuccess = ref(false);
|
const amtTestSuccess = ref(false);
|
||||||
|
|
||||||
// 网络扫描相关
|
|
||||||
const scanFormData = reactive({
|
|
||||||
networkAddress: '',
|
|
||||||
subnetMask: '',
|
|
||||||
credentialId: null as string | null
|
|
||||||
});
|
|
||||||
const scanning = ref(false);
|
|
||||||
const scanResult = ref<Api.Device.NetworkScanResult | null>(null);
|
|
||||||
const selectedScanDevices = ref<string[]>([]);
|
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
id: null as number | null,
|
id: null as number | null,
|
||||||
deviceName: '',
|
deviceName: '',
|
||||||
@ -687,7 +474,8 @@ const formData = reactive({
|
|||||||
|
|
||||||
const formRules = {
|
const formRules = {
|
||||||
deviceName: { required: true, message: '请输入设备名称', trigger: 'blur' },
|
deviceName: { required: true, message: '请输入设备名称', trigger: 'blur' },
|
||||||
deviceCode: { required: true, message: '请输入UUID', trigger: 'blur' }
|
deviceCode: { required: true, message: '请输入UUID', trigger: 'blur' },
|
||||||
|
status: { required: true, message: '请选择设备状态', trigger: 'change' }
|
||||||
};
|
};
|
||||||
|
|
||||||
// 详情弹窗
|
// 详情弹窗
|
||||||
@ -875,8 +663,7 @@ async function handleTestAmtConnection() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (amtFormData.useCredential && amtFormData.credentialId) {
|
if (amtFormData.useCredential && amtFormData.credentialId) {
|
||||||
// 直接使用字符串 ID,不转换为数字
|
requestData.credentialId = amtFormData.credentialId;
|
||||||
requestData.credentialId = amtFormData.credentialId as any;
|
|
||||||
} else {
|
} else {
|
||||||
requestData.username = amtFormData.username;
|
requestData.username = amtFormData.username;
|
||||||
requestData.password = amtFormData.password;
|
requestData.password = amtFormData.password;
|
||||||
@ -888,10 +675,7 @@ async function handleTestAmtConnection() {
|
|||||||
amtTestSuccess.value = true;
|
amtTestSuccess.value = true;
|
||||||
window.$message?.success('AMT 连接测试成功');
|
window.$message?.success('AMT 连接测试成功');
|
||||||
|
|
||||||
// 先填充 IP 地址
|
// 自动获取设备信息
|
||||||
formData.ipAddress = amtFormData.ipAddress;
|
|
||||||
|
|
||||||
// 自动获取设备信息(包括 UUID、设备名称、MAC 地址)
|
|
||||||
await handleGetAmtDeviceInfo();
|
await handleGetAmtDeviceInfo();
|
||||||
} else {
|
} else {
|
||||||
window.$message?.error('AMT 连接测试失败');
|
window.$message?.error('AMT 连接测试失败');
|
||||||
@ -911,8 +695,7 @@ async function handleGetAmtDeviceInfo() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (amtFormData.useCredential && amtFormData.credentialId) {
|
if (amtFormData.useCredential && amtFormData.credentialId) {
|
||||||
// 直接使用字符串 ID,不转换为数字
|
requestData.credentialId = amtFormData.credentialId;
|
||||||
requestData.credentialId = amtFormData.credentialId as any;
|
|
||||||
} else {
|
} else {
|
||||||
requestData.username = amtFormData.username;
|
requestData.username = amtFormData.username;
|
||||||
requestData.password = amtFormData.password;
|
requestData.password = amtFormData.password;
|
||||||
@ -943,117 +726,4 @@ function handleCredentialToggle(value: boolean) {
|
|||||||
amtTestSuccess.value = false;
|
amtTestSuccess.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始网络扫描
|
|
||||||
async function handleStartScan() {
|
|
||||||
// 验证必填项
|
|
||||||
if (!scanFormData.networkAddress) {
|
|
||||||
window.$message?.warning('请输入网络地址');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scanFormData.subnetMask) {
|
|
||||||
window.$message?.warning('请输入子网掩码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scanFormData.credentialId) {
|
|
||||||
window.$message?.warning('请选择 AMT 凭证');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scanning.value = true;
|
|
||||||
scanResult.value = null;
|
|
||||||
selectedScanDevices.value = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requestData: Api.Device.NetworkScanRequest = {
|
|
||||||
networkAddress: scanFormData.networkAddress,
|
|
||||||
subnetMask: scanFormData.subnetMask,
|
|
||||||
credentialId: scanFormData.credentialId
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data } = await fetchScanNetwork(requestData);
|
|
||||||
scanResult.value = data;
|
|
||||||
|
|
||||||
if (data.foundDevices > 0) {
|
|
||||||
window.$message?.success(`扫描完成,发现 ${data.foundDevices} 个 AMT 设备`);
|
|
||||||
} else {
|
|
||||||
window.$message?.info('扫描完成,未发现 AMT 设备');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
window.$message?.error(error?.message || '网络扫描失败');
|
|
||||||
scanResult.value = null;
|
|
||||||
} finally {
|
|
||||||
scanning.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择扫描到的设备
|
|
||||||
function handleScanDeviceCheck(keys: string[]) {
|
|
||||||
selectedScanDevices.value = keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换单个扫描设备的选择状态
|
|
||||||
function handleScanDeviceToggle(ipAddress: string, checked: boolean) {
|
|
||||||
if (checked) {
|
|
||||||
if (!selectedScanDevices.value.includes(ipAddress)) {
|
|
||||||
selectedScanDevices.value.push(ipAddress);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedScanDevices.value = selectedScanDevices.value.filter(ip => ip !== ipAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量添加扫描到的设备
|
|
||||||
async function handleBatchAddScannedDevices() {
|
|
||||||
if (selectedScanDevices.value.length === 0) {
|
|
||||||
window.$message?.warning('请选择要添加的设备');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const devicesToAdd = scanResult.value?.devices.filter(
|
|
||||||
device => selectedScanDevices.value.includes(device.ipAddress) && device.status === 'success'
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
if (devicesToAdd.length === 0) {
|
|
||||||
window.$message?.warning('没有可添加的设备');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
submitLoading.value = true;
|
|
||||||
let successCount = 0;
|
|
||||||
let failCount = 0;
|
|
||||||
|
|
||||||
for (const device of devicesToAdd) {
|
|
||||||
try {
|
|
||||||
await fetchCreateDevice({
|
|
||||||
deviceName: device.deviceName,
|
|
||||||
deviceCode: device.deviceCode,
|
|
||||||
status: 'online',
|
|
||||||
ipAddress: device.ipAddress,
|
|
||||||
macAddress: device.macAddress,
|
|
||||||
remark: '通过网络扫描添加'
|
|
||||||
});
|
|
||||||
successCount++;
|
|
||||||
} catch (error) {
|
|
||||||
failCount++;
|
|
||||||
console.error(`添加设备 ${device.ipAddress} 失败:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (successCount > 0) {
|
|
||||||
window.$message?.success(`成功添加 ${successCount} 个设备${failCount > 0 ? `,${failCount} 个失败` : ''}`);
|
|
||||||
modalVisible.value = false;
|
|
||||||
loadData();
|
|
||||||
} else {
|
|
||||||
window.$message?.error('所有设备添加失败');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
window.$message?.error(error?.message || '批量添加失败');
|
|
||||||
} finally {
|
|
||||||
submitLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,179 +0,0 @@
|
|||||||
# 网络扫描功能测试指南
|
|
||||||
|
|
||||||
## 功能说明
|
|
||||||
|
|
||||||
网络扫描功能允许用户:
|
|
||||||
1. 输入网络地址和子网掩码来定义扫描范围
|
|
||||||
2. 选择 AMT 凭证进行批量设备发现
|
|
||||||
3. 在右侧列表中查看扫描到的设备
|
|
||||||
4. 批量选择并添加设备到系统
|
|
||||||
|
|
||||||
**注意**:AMT 自动添加模式下,不需要手动输入设备信息,所有信息都从扫描结果中获取。
|
|
||||||
|
|
||||||
## 测试步骤
|
|
||||||
|
|
||||||
### 1. 重新编译后端
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
mvn clean package -DskipTests
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 启动后端服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
java -jar target/soybean-admin-1.0.0.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
或使用批处理文件:
|
|
||||||
```bash
|
|
||||||
start_backend.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 启动前端服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 测试网络扫描
|
|
||||||
|
|
||||||
1. 登录系统
|
|
||||||
2. 进入"设备管理" -> "设备列表"
|
|
||||||
3. 点击"新增设备"按钮
|
|
||||||
4. 选择"AMT 自动添加"方式
|
|
||||||
|
|
||||||
#### 左侧:扫描配置
|
|
||||||
- **网络地址**:输入网络号,例如 `192.168.8.0`
|
|
||||||
- **子网掩码**:输入子网掩码,支持两种格式:
|
|
||||||
- 点分十进制:`255.255.255.0`
|
|
||||||
- CIDR 格式:`/24`
|
|
||||||
- **使用凭证**:选择已保存的 AMT 凭证(如"默认管理员凭证")
|
|
||||||
- 点击"开始扫描"按钮
|
|
||||||
|
|
||||||
#### 右侧:扫描结果
|
|
||||||
- 扫描过程中会显示进度条
|
|
||||||
- 扫描完成后,右侧列表会显示发现的设备
|
|
||||||
- 每个设备显示:
|
|
||||||
- 设备名称
|
|
||||||
- IP 地址
|
|
||||||
- UUID
|
|
||||||
- MAC 地址
|
|
||||||
- 状态标签(成功/失败)
|
|
||||||
|
|
||||||
#### 批量添加
|
|
||||||
1. 在右侧列表中勾选要添加的设备
|
|
||||||
2. 底部显示已选择的设备数量
|
|
||||||
3. 点击"批量添加"按钮
|
|
||||||
4. 系统会自动将选中的设备添加到设备列表
|
|
||||||
5. 弹窗自动关闭
|
|
||||||
|
|
||||||
**注意**:
|
|
||||||
- AMT 自动添加模式下,不显示设备信息输入框
|
|
||||||
- 所有设备信息(名称、UUID、IP、MAC)都从扫描结果自动获取
|
|
||||||
- 批量添加后,弹窗会自动关闭,无需点击"确定"按钮
|
|
||||||
|
|
||||||
## 测试场景
|
|
||||||
|
|
||||||
### 场景 1:小网段扫描
|
|
||||||
- 网络地址:`192.168.8.0`
|
|
||||||
- 子网掩码:`/24` 或 `255.255.255.0`
|
|
||||||
- 预期:扫描 254 个 IP(192.168.8.1 - 192.168.8.254)
|
|
||||||
|
|
||||||
### 场景 2:更小的网段
|
|
||||||
- 网络地址:`192.168.8.0`
|
|
||||||
- 子网掩码:`/28` 或 `255.255.255.240`
|
|
||||||
- 预期:扫描 14 个 IP
|
|
||||||
|
|
||||||
### 场景 3:单个 IP
|
|
||||||
- 网络地址:`192.168.8.112`
|
|
||||||
- 子网掩码:`/32` 或 `255.255.255.255`
|
|
||||||
- 预期:只扫描一个 IP
|
|
||||||
|
|
||||||
## 预期结果
|
|
||||||
|
|
||||||
### 扫描进度
|
|
||||||
- 进度条实时显示扫描进度
|
|
||||||
- 显示"已扫描 X / 总数 Y"
|
|
||||||
- 显示"发现设备: Z"
|
|
||||||
|
|
||||||
### 扫描结果列表
|
|
||||||
- 左侧显示扫描配置
|
|
||||||
- 右侧显示设备列表(卡片式布局)
|
|
||||||
- 每个设备可以单独勾选
|
|
||||||
- 底部显示选择统计和批量添加按钮
|
|
||||||
|
|
||||||
### 批量添加
|
|
||||||
- 成功添加后显示成功消息
|
|
||||||
- 自动关闭弹窗
|
|
||||||
- 刷新设备列表
|
|
||||||
- 新添加的设备状态为"在线"
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **扫描时间**:取决于网段大小和网络状况
|
|
||||||
- /24 网段(254个IP):约 2-5 分钟
|
|
||||||
- /28 网段(14个IP):约 10-30 秒
|
|
||||||
|
|
||||||
2. **超时设置**:后端设置了 5 分钟超时
|
|
||||||
|
|
||||||
3. **并发扫描**:使用 10 个线程并发扫描,提高效率
|
|
||||||
|
|
||||||
4. **错误处理**:
|
|
||||||
- 无法连接的 IP 会被跳过
|
|
||||||
- 非 AMT 设备会被标记为失败
|
|
||||||
- 只有成功连接的设备才会显示在列表中
|
|
||||||
|
|
||||||
## 故障排查
|
|
||||||
|
|
||||||
### 问题 1:扫描无结果
|
|
||||||
- 检查网络地址和子网掩码是否正确
|
|
||||||
- 确认 AMT 设备在该网段内
|
|
||||||
- 检查凭证是否正确
|
|
||||||
|
|
||||||
### 问题 2:扫描超时
|
|
||||||
- 网段太大,考虑缩小范围
|
|
||||||
- 网络延迟较高
|
|
||||||
- 检查后端日志
|
|
||||||
|
|
||||||
### 问题 3:进度条不更新
|
|
||||||
- 这是正常的,因为后端是同步扫描
|
|
||||||
- 扫描完成后会一次性返回所有结果
|
|
||||||
|
|
||||||
### 问题 4:批量添加失败
|
|
||||||
- 检查设备 UUID 是否重复
|
|
||||||
- 查看后端日志获取详细错误信息
|
|
||||||
- 确认数据库连接正常
|
|
||||||
|
|
||||||
## 后端日志查看
|
|
||||||
|
|
||||||
扫描过程中,后端会输出详细日志:
|
|
||||||
```
|
|
||||||
开始扫描网络: 192.168.8.0/24
|
|
||||||
生成 IP 列表,共 254 个 IP
|
|
||||||
发现 AMT 设备: 192.168.8.112 (DESKTOP-ABC123)
|
|
||||||
扫描完成,发现 3 个设备
|
|
||||||
```
|
|
||||||
|
|
||||||
## UI 改进说明
|
|
||||||
|
|
||||||
### 新的布局
|
|
||||||
- 使用 Grid 布局,左右分栏
|
|
||||||
- 左侧:扫描配置(固定宽度)
|
|
||||||
- 右侧:扫描结果列表(自适应)
|
|
||||||
- AMT 自动添加模式:不显示设备信息输入框
|
|
||||||
- 手动添加模式:显示完整的设备信息表单
|
|
||||||
|
|
||||||
### 扫描结果展示
|
|
||||||
- 使用 n-list 组件替代 n-data-table
|
|
||||||
- 卡片式布局,更直观
|
|
||||||
- 每个设备显示完整信息
|
|
||||||
- 支持单个勾选和批量操作
|
|
||||||
- 批量添加后自动关闭弹窗
|
|
||||||
|
|
||||||
### 进度显示
|
|
||||||
- 进度条显示扫描百分比
|
|
||||||
- 实时显示已扫描数量
|
|
||||||
- 显示发现的设备数量
|
|
||||||
- 扫描完成后显示成功标签
|
|
||||||
Loading…
x
Reference in New Issue
Block a user