feat: 拆分设备状态为AMT状态和Agent状态

- 数据库:添加 amt_status 和 agent_status 字段
- 后端:Device 实体类和 DeviceDTO 添加新状态字段
- 后端:DeviceService 添加状态检测和更新方法
- 后端:添加 AmtStatusCheckTask 定时任务(每30秒检测一次)
- 前端:设备列表页面拆分状态列显示
- 前端:统计卡片显示 AMT 和 Agent 在线/离线数量
- 网络扫描:自动设置 AMT 状态为在线
- 文档:添加 DEVICE_STATUS_SPLIT.md 和 AMT_REALTIME_STATUS.md
This commit is contained in:
lvfengfree 2026-03-01 19:18:32 +08:00
parent 028fd8f444
commit 2e28ad1472
16 changed files with 1417 additions and 22 deletions

417
AMT_REALTIME_STATUS.md Normal file
View File

@ -0,0 +1,417 @@
# AMT 实时状态检测功能
## 功能概述
实现了 AMT 设备状态的实时检测和自动更新功能,无需手动刷新即可看到设备的在线/离线状态变化。
## 实现方案
### 后端实现
#### 1. 定时任务 (AmtStatusCheckTask.java)
**位置**: `backend/src/main/java/com/soybean/admin/task/AmtStatusCheckTask.java`
**功能**:
- 每 30 秒自动检测一次所有设备的 AMT 状态
- 使用线程池并发检测,提高效率
- 只在状态变化时更新数据库,减少写入操作
- 使用 Socket 连接检测 AMT 端口16992/16993可用性
**关键特性**:
```java
@Scheduled(fixedRate = 30000, initialDelay = 10000)
public void checkAmtStatus() {
// 每 30 秒执行一次
// 初始延迟 10 秒,等待应用完全启动
}
```
**检测逻辑**:
1. 查询所有有 IP 地址的设备
2. 并发检测每个设备的 AMT 端口连接性
3. 先尝试 HTTP (16992),失败则尝试 HTTPS (16993)
4. 根据连接结果更新设备的 `amt_status` 字段
5. 只有状态变化时才更新数据库和记录日志
**线程池配置**:
- 固定 20 个线程
- 使用 CompletableFuture 异步执行
- 等待所有检测完成后结束任务
#### 2. 启用定时任务
**修改**: `backend/src/main/java/com/soybean/admin/SoybeanAdminApplication.java`
添加 `@EnableScheduling` 注解启用 Spring 定时任务功能:
```java
@SpringBootApplication
@MapperScan("com.soybean.admin.mapper")
@EnableScheduling // 启用定时任务
public class SoybeanAdminApplication {
// ...
}
```
### 前端实现
#### 1. 自动轮询刷新
**位置**: `src/views/device/list/index.vue`
**功能**:
- 页面加载后每 10 秒自动刷新设备列表
- 组件卸载时自动清除定时器
- 显示"自动刷新中"状态标签
**实现代码**:
```typescript
onMounted(() => {
loadData();
// 启动定时刷新,每 10 秒刷新一次设备状态
const refreshInterval = setInterval(() => {
loadData();
}, 10000);
// 组件卸载时清除定时器
onUnmounted(() => {
clearInterval(refreshInterval);
});
});
```
#### 2. 手动刷新按钮
添加了"刷新状态"按钮,用户可以随时手动刷新:
```vue
<n-button @click="handleManualRefresh" :loading="loading">
<icon-mdi-refresh class="mr-4px text-16px" />
刷新状态
</n-button>
```
#### 3. 状态指示器
显示自动刷新状态:
```vue
<n-tag v-if="autoRefreshEnabled" type="success" size="small">
<icon-mdi-autorenew class="mr-4px" />
自动刷新中
</n-tag>
```
## 工作流程
### 完整流程图
```
后端定时任务 (每30秒)
检测所有设备 AMT 端口
更新数据库状态
前端轮询 (每10秒)
获取最新设备列表
更新界面显示
```
### 时间线示例
```
T=0s: 后端启动10秒后开始第一次检测
T=10s: 后端第一次检测所有设备
T=10s: 前端页面加载,开始第一次数据获取
T=20s: 前端第二次刷新
T=30s: 前端第三次刷新
T=40s: 后端第二次检测 + 前端第四次刷新
T=50s: 前端第五次刷新
...
```
## 性能优化
### 后端优化
1. **并发检测**: 使用 20 个线程并发检测,大幅提升检测速度
2. **快速超时**: Socket 连接超时设置为 3 秒,避免长时间等待
3. **增量更新**: 只在状态变化时更新数据库
4. **简单检测**: 只检测端口连接性,不进行完整的 AMT 认证
### 前端优化
1. **合理间隔**: 10 秒刷新间隔,平衡实时性和性能
2. **自动清理**: 组件卸载时清除定时器,避免内存泄漏
3. **加载状态**: 显示加载状态,提升用户体验
## 状态检测逻辑
### 检测方法
使用 TCP Socket 连接检测,而不是完整的 AMT 认证:
```java
private boolean checkAmtConnectivity(String ipAddress) {
try {
// 尝试连接 AMT HTTP 端口
Socket socket = new Socket();
socket.connect(new InetSocketAddress(ipAddress, 16992), 3000);
socket.close();
return true;
} catch (Exception e) {
// HTTP 失败,尝试 HTTPS
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(ipAddress, 16993), 3000);
socket.close();
return true;
} catch (Exception ex) {
return false;
}
}
}
```
### 优点
1. **快速**: 不需要完整的 SOAP 请求和认证
2. **轻量**: 只检测端口可用性
3. **可靠**: 能准确判断 AMT 服务是否运行
### 缺点
1. **不验证认证**: 不检查凭证是否正确
2. **端口占用**: 如果端口被其他服务占用,可能误判
## 使用说明
### 1. 执行数据库更新
如果还没有执行过数据库更新,先运行:
```bash
update_device_status.bat
```
### 2. 重新编译后端
```bash
cd backend
mvn clean package -DskipTests
```
### 3. 启动后端服务
```bash
start_backend.bat
```
后端启动后,会在 10 秒后开始第一次 AMT 状态检测,之后每 30 秒检测一次。
### 4. 打开前端页面
访问设备列表页面,会看到:
- 右上角显示"自动刷新中"标签
- 设备的 AMT 状态列实时更新
- 可以点击"刷新状态"按钮手动刷新
## 日志查看
### 后端日志
查看后端控制台输出,会看到类似日志:
```
开始 AMT 状态检测任务
开始检测 10 个设备的 AMT 状态
设备 Server-01 (192.168.8.100) AMT 状态变化: offline -> online
设备 Server-02 (192.168.8.101) AMT 状态变化: online -> offline
AMT 状态检测任务完成
```
### 前端日志
打开浏览器开发者工具,可以看到每 10 秒发送一次设备列表请求。
## 测试场景
### 场景 1: 设备上线
1. 启动一台 AMT 设备
2. 等待最多 30 秒(后端检测周期)
3. 前端会在下次刷新时显示设备状态变为"在线"
### 场景 2: 设备下线
1. 关闭一台 AMT 设备
2. 等待最多 30 秒(后端检测周期)
3. 前端会在下次刷新时显示设备状态变为"离线"
### 场景 3: 手动刷新
1. 点击"刷新状态"按钮
2. 立即获取最新的设备状态
3. 不需要等待自动刷新周期
## 配置参数
### 后端配置
`AmtStatusCheckTask.java` 中可以调整:
```java
// 检测周期(毫秒)
@Scheduled(fixedRate = 30000, initialDelay = 10000)
// fixedRate: 30000 = 30秒
// initialDelay: 10000 = 10秒初始延迟
// 线程池大小
private final ExecutorService executorService = Executors.newFixedThreadPool(20);
// 20 个线程
// Socket 超时时间(毫秒)
socket.connect(new InetSocketAddress(ipAddress, 16992), 3000);
// 3000 = 3秒
```
### 前端配置
`index.vue` 中可以调整:
```typescript
// 刷新周期(毫秒)
const refreshInterval = setInterval(() => {
loadData();
}, 10000); // 10000 = 10秒
```
## 性能影响
### 后端资源消耗
- **CPU**: 低(主要是网络 I/O 等待)
- **内存**: 低20 个线程 + 设备列表)
- **网络**: 每个设备每 30 秒一次连接检测
### 前端资源消耗
- **网络**: 每 10 秒一次 API 请求
- **内存**: 低(只更新设备列表数据)
- **CPU**: 低(简单的数据更新和渲染)
### 数据库影响
- **读取**: 每 30 秒读取所有设备列表
- **写入**: 只在状态变化时写入(增量更新)
- **索引**: 建议在 `ip_address``amt_status` 字段上建立索引
## 后续优化建议
### 1. WebSocket 推送
替代轮询方式,使用 WebSocket 实时推送状态变化:
**优点**:
- 更实时
- 减少网络请求
- 降低服务器负载
**实现**:
- 后端使用 Spring WebSocket
- 状态变化时主动推送到前端
- 前端监听 WebSocket 消息更新界面
### 2. 智能检测频率
根据设备状态调整检测频率:
- 在线设备: 每 60 秒检测一次
- 离线设备: 每 30 秒检测一次
- 新添加设备: 每 10 秒检测一次(前 5 分钟)
### 3. 批量状态查询接口
添加专门的状态查询接口,只返回状态信息:
```java
@GetMapping("/device/status")
public Result<Map<Long, String>> getDeviceStatus() {
// 只返回 id -> status 的映射
// 减少数据传输量
}
```
### 4. 状态变化通知
设备状态变化时发送通知:
- 邮件通知
- 短信通知
- 系统消息通知
- Webhook 回调
### 5. 历史状态记录
记录设备状态变化历史:
- 创建 `device_status_history`
- 记录每次状态变化的时间和原因
- 生成可用性报表
## 故障排查
### 问题 1: 状态不更新
**可能原因**:
- 后端定时任务未启动
- 数据库连接失败
- 网络不通
**排查方法**:
1. 查看后端日志是否有"开始 AMT 状态检测任务"
2. 检查数据库 `amt_status` 字段是否存在
3. 手动测试设备网络连接
### 问题 2: 前端不刷新
**可能原因**:
- 定时器未启动
- API 请求失败
- 组件已卸载
**排查方法**:
1. 打开浏览器开发者工具查看网络请求
2. 检查控制台是否有错误
3. 确认 `autoRefreshEnabled` 为 true
### 问题 3: 性能问题
**可能原因**:
- 设备数量过多
- 检测频率过高
- 网络延迟大
**解决方案**:
1. 增加检测周期(如改为 60 秒)
2. 增加线程池大小
3. 使用 WebSocket 替代轮询
## 总结
实现了完整的 AMT 实时状态检测功能:
✅ 后端定时任务自动检测(每 30 秒)
✅ 前端自动刷新显示(每 10 秒)
✅ 手动刷新按钮
✅ 状态指示器
✅ 并发检测优化
✅ 增量数据库更新
✅ 完整的日志记录
用户无需手动刷新,即可实时看到设备的 AMT 在线/离线状态变化。

283
DEVICE_STATUS_SPLIT.md Normal file
View File

@ -0,0 +1,283 @@
# 设备状态拆分说明
## 功能说明
将原来的单一设备状态拆分为两个独立的状态字段:
1. **AMT 状态** (`amt_status`):表示 Intel AMT 是否在线
- `online`AMT 在线,可以进行远程管理
- `offline`AMT 离线,无法连接
2. **Agent 状态** (`agent_status`):表示操作系统是否在线
- `online`操作系统在线Agent 正常运行
- `offline`:操作系统离线或 Agent 未运行
## 数据库变更
### 新增字段
```sql
ALTER TABLE device
ADD COLUMN amt_status VARCHAR(20) DEFAULT 'offline' COMMENT 'AMT状态: online-在线, offline-离线',
ADD COLUMN agent_status VARCHAR(20) DEFAULT 'offline' COMMENT 'Agent状态: online-在线, offline-离线';
```
### 数据迁移
- 原 `status` 字段保留用于兼容
- 新设备的 `amt_status``agent_status` 默认为 `offline`
- 通过网络扫描添加的设备,`amt_status` 自动设置为 `online`
## 后端变更
### 1. 实体类更新
**Device.java**:
```java
private String status; // 保留原字段用于兼容
private String amtStatus; // AMT 状态
private String agentStatus; // Agent 状态
```
**DeviceDTO.java**:
```java
private String status; // 保留用于兼容
private String amtStatus; // AMT 状态
private String agentStatus; // Agent 状态
```
### 2. 服务层更新
**DeviceService.java**:
- 添加 `checkAmtStatus()` 方法:检测设备 AMT 状态
- 添加 `updateAmtStatus()` 方法:更新设备 AMT 状态
- 添加 `updateAgentStatus()` 方法:更新设备 Agent 状态
- 更新 `getDeviceStatistics()` 方法:统计 AMT 和 Agent 在线/离线数量
## 前端变更
### 1. 类型定义更新
**device.d.ts**:
```typescript
interface Device {
status: 'online' | 'offline' | 'fault' | 'maintenance'; // 保留
amtStatus: 'online' | 'offline'; // AMT 状态
agentStatus: 'online' | 'offline'; // Agent 状态
}
```
### 2. 设备列表页面更新
**index.vue**:
#### 统计卡片
- 总设备数
- AMT 在线
- AMT 离线
- Agent 在线
- Agent 离线
#### 表格列
- 设备名称
- UUID
- **AMT 状态**(新增)
- **Agent 状态**(新增)
- IP 地址
- MAC 地址
- 创建时间
- 操作
#### 状态显示
- AMT 在线:绿色标签
- AMT 离线:灰色标签
- Agent 在线:蓝色标签
- Agent 离线:灰色标签
### 3. 网络扫描功能
- 扫描到的设备自动设置 `amtStatus = 'online'`
- Agent 状态默认为 `offline`(需要 Agent 上报)
## 使用说明
### 1. 执行数据库更新
运行批处理文件:
```bash
update_device_status.bat
```
或手动执行 SQL
```bash
mysql -h localhost -P 3306 -u root -proot soybean_admin < update_device_status_fields.sql
```
### 2. 重新编译后端
```bash
cd backend
mvn clean package -DskipTests
```
### 3. 重启后端服务
```bash
start_backend.bat
```
### 4. 测试功能
1. 打开设备列表页面
2. 查看统计卡片是否显示 AMT 和 Agent 状态
3. 查看表格是否显示两个状态列
4. 进行网络扫描,添加设备
5. 验证新添加的设备 AMT 状态为"在线"
## AMT 状态检测
### 当前实现
扫描到的设备自动设置为在线,其他设备默认离线。
### 后续完善
1. **定时检测**
- 创建定时任务,定期检测所有设备的 AMT 状态
- 使用 Identify 请求快速检测连接性
- 更新数据库中的 `amt_status` 字段
2. **手动刷新**
- 在设备列表添加"刷新状态"按钮
- 点击后检测选中设备的 AMT 状态
- 实时更新状态显示
3. **状态变化通知**
- AMT 从在线变为离线时发送告警
- 记录状态变化历史
## Agent 状态实现
### 当前状态
Agent 状态字段已添加,默认为离线(假数据)。
### 后续实现步骤
1. **开发 Agent 程序**
- 在目标设备上运行的客户端程序
- 定期向服务器报告心跳
- 上报设备信息CPU、内存、磁盘等
2. **心跳机制**
- Agent 每 30 秒发送一次心跳
- 服务器记录最后心跳时间
- 超过 2 分钟未收到心跳则标记为离线
3. **状态更新接口**
```java
@PostMapping("/device/agent/heartbeat")
public Result<Boolean> agentHeartbeat(@RequestBody AgentHeartbeat heartbeat) {
// 更新设备 Agent 状态为在线
// 更新最后心跳时间
// 记录设备信息
}
```
4. **定时检查**
- 创建定时任务,每分钟检查一次
- 将超时未心跳的设备标记为离线
## 状态组合说明
| AMT 状态 | Agent 状态 | 说明 |
|---------|-----------|------|
| 在线 | 在线 | 设备完全正常,可进行所有操作 |
| 在线 | 离线 | 操作系统关机,但 AMT 可用,可远程开机 |
| 离线 | 在线 | AMT 未启用或故障,但系统正常运行 |
| 离线 | 离线 | 设备完全离线或断电 |
## 操作建议
### 根据状态执行操作
1. **AMT 在线 + Agent 离线**
- 可以使用远程开机功能
- 可以查看 BIOS 设置
- 可以进行硬件级别的管理
2. **AMT 离线 + Agent 在线**
- 可以通过 Agent 执行软件操作
- 无法进行硬件级别的管理
- 建议启用 AMT 功能
3. **AMT 在线 + Agent 在线**
- 所有功能可用
- 最佳状态
4. **AMT 离线 + Agent 离线**
- 设备不可用
- 需要物理检查设备
## 测试场景
### 场景 1网络扫描添加设备
1. 执行网络扫描
2. 选择设备并批量添加
3. 验证AMT 状态 = 在线Agent 状态 = 离线
### 场景 2手动添加设备
1. 选择"手动添加"方式
2. 输入设备信息
3. 验证AMT 状态 = 离线Agent 状态 = 离线
### 场景 3查看统计信息
1. 打开设备列表
2. 查看顶部统计卡片
3. 验证:显示 AMT 和 Agent 的在线/离线数量
### 场景 4状态筛选
1. 在搜索栏选择状态筛选
2. 验证:可以按 AMT 或 Agent 状态筛选(后续实现)
## 注意事项
1. **数据兼容性**
- 保留了原 `status` 字段
- 旧代码仍可正常运行
- 建议逐步迁移到新字段
2. **性能考虑**
- AMT 状态检测会产生网络请求
- 建议使用缓存和定时任务
- 避免频繁检测
3. **错误处理**
- AMT 检测失败不应影响其他功能
- 记录检测失败的原因
- 提供重试机制
4. **安全性**
- Agent 心跳需要身份验证
- 防止伪造心跳攻击
- 加密传输敏感信息
## 后续优化
1. **实时状态推送**
- 使用 WebSocket 推送状态变化
- 前端实时更新显示
2. **状态历史记录**
- 记录状态变化历史
- 生成状态变化报表
- 分析设备可用性
3. **告警功能**
- 设备离线告警
- 状态异常告警
- 邮件/短信通知
4. **批量操作**
- 批量刷新状态
- 批量启用/禁用 AMT
- 批量部署 Agent

View File

@ -3,9 +3,11 @@ package com.soybean.admin;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@MapperScan("com.soybean.admin.mapper") @MapperScan("com.soybean.admin.mapper")
@EnableScheduling
public class SoybeanAdminApplication { public class SoybeanAdminApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(SoybeanAdminApplication.class, args); SpringApplication.run(SoybeanAdminApplication.class, args);

View File

@ -7,7 +7,9 @@ public class DeviceDTO {
private Long id; private Long id;
private String deviceName; private String deviceName;
private String deviceCode; private String deviceCode;
private String status; private String status; // 保留用于兼容
private String amtStatus; // AMT 状态
private String agentStatus; // Agent 状态
private String ipAddress; private String ipAddress;
private String macAddress; private String macAddress;
private String remark; private String remark;

View File

@ -15,7 +15,9 @@ public class Device {
private String deviceName; private String deviceName;
private String deviceCode; private String deviceCode;
private String status; private String status; // 保留原字段用于兼容
private String amtStatus; // AMT 状态online, offline
private String agentStatus; // Agent 状态online, offline
private String ipAddress; private String ipAddress;
private String macAddress; private String macAddress;
private String remark; private String remark;

View File

@ -21,6 +21,9 @@ public class DeviceService {
@Autowired @Autowired
private DeviceMapper deviceMapper; private DeviceMapper deviceMapper;
@Autowired
private AmtDigestService amtDigestService;
public PageResponse<Device> getDeviceList(PageRequest pageRequest) { public PageResponse<Device> getDeviceList(PageRequest pageRequest) {
Page<Device> page = new Page<>(pageRequest.getCurrent(), pageRequest.getSize()); Page<Device> page = new Page<>(pageRequest.getCurrent(), pageRequest.getSize());
@ -102,26 +105,87 @@ public class DeviceService {
// 总数 // 总数
statistics.put("total", deviceMapper.selectCount(null)); statistics.put("total", deviceMapper.selectCount(null));
// 在线 // AMT 在线
wrapper.clear();
wrapper.eq(Device::getAmtStatus, "online");
statistics.put("amtOnline", deviceMapper.selectCount(wrapper));
// AMT 离线
wrapper.clear();
wrapper.eq(Device::getAmtStatus, "offline");
statistics.put("amtOffline", deviceMapper.selectCount(wrapper));
// Agent 在线
wrapper.clear();
wrapper.eq(Device::getAgentStatus, "online");
statistics.put("agentOnline", deviceMapper.selectCount(wrapper));
// Agent 离线
wrapper.clear();
wrapper.eq(Device::getAgentStatus, "offline");
statistics.put("agentOffline", deviceMapper.selectCount(wrapper));
// 兼容旧的统计基于 status 字段
wrapper.clear(); wrapper.clear();
wrapper.eq(Device::getStatus, "online"); wrapper.eq(Device::getStatus, "online");
statistics.put("online", deviceMapper.selectCount(wrapper)); statistics.put("online", deviceMapper.selectCount(wrapper));
// 离线
wrapper.clear(); wrapper.clear();
wrapper.eq(Device::getStatus, "offline"); wrapper.eq(Device::getStatus, "offline");
statistics.put("offline", deviceMapper.selectCount(wrapper)); statistics.put("offline", deviceMapper.selectCount(wrapper));
// 故障
wrapper.clear(); wrapper.clear();
wrapper.eq(Device::getStatus, "fault"); wrapper.eq(Device::getStatus, "fault");
statistics.put("fault", deviceMapper.selectCount(wrapper)); statistics.put("fault", deviceMapper.selectCount(wrapper));
// 维护中
wrapper.clear();
wrapper.eq(Device::getStatus, "maintenance");
statistics.put("maintenance", deviceMapper.selectCount(wrapper));
return statistics; return statistics;
} }
/**
* 检测设备 AMT 状态
*/
public String checkAmtStatus(String ipAddress) {
if (ipAddress == null || ipAddress.isEmpty()) {
return "offline";
}
try {
// 使用简单的 Identify 请求检测 AMT 是否在线
// 这里需要一个默认凭证或者从设备记录中获取凭证
// 暂时返回离线后续完善
return "offline";
} catch (Exception e) {
return "offline";
}
}
/**
* 更新设备 AMT 状态
*/
public boolean updateAmtStatus(Long deviceId, String amtStatus) {
Device device = deviceMapper.selectById(deviceId);
if (device == null) {
return false;
}
device.setAmtStatus(amtStatus);
device.setUpdateTime(LocalDateTime.now());
return deviceMapper.updateById(device) > 0;
}
/**
* 更新设备 Agent 状态
*/
public boolean updateAgentStatus(Long deviceId, String agentStatus) {
Device device = deviceMapper.selectById(deviceId);
if (device == null) {
return false;
}
device.setAgentStatus(agentStatus);
device.setUpdateTime(LocalDateTime.now());
return deviceMapper.updateById(device) > 0;
}
} }

View File

@ -0,0 +1,126 @@
package com.soybean.admin.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.soybean.admin.dto.AmtTestRequest;
import com.soybean.admin.entity.Device;
import com.soybean.admin.mapper.DeviceMapper;
import com.soybean.admin.service.AmtDigestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* AMT 状态检测定时任务
* 30 秒检测一次所有设备的 AMT 状态
*/
@Component
public class AmtStatusCheckTask {
private static final Logger logger = LoggerFactory.getLogger(AmtStatusCheckTask.class);
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private AmtDigestService amtDigestService;
// 线程池用于并发检测
private final ExecutorService executorService = Executors.newFixedThreadPool(20);
/**
* 30 秒执行一次 AMT 状态检测
*/
@Scheduled(fixedRate = 30000, initialDelay = 10000)
public void checkAmtStatus() {
logger.info("开始 AMT 状态检测任务");
try {
// 查询所有有 IP 地址的设备
LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
wrapper.isNotNull(Device::getIpAddress);
wrapper.ne(Device::getIpAddress, "");
List<Device> devices = deviceMapper.selectList(wrapper);
if (devices.isEmpty()) {
logger.info("没有需要检测的设备");
return;
}
logger.info("开始检测 {} 个设备的 AMT 状态", devices.size());
// 并发检测所有设备
List<CompletableFuture<Void>> futures = devices.stream()
.map(device -> CompletableFuture.runAsync(() -> checkSingleDevice(device), executorService))
.toList();
// 等待所有检测完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
logger.info("AMT 状态检测任务完成");
} catch (Exception e) {
logger.error("AMT 状态检测任务失败", e);
}
}
/**
* 检测单个设备的 AMT 状态
*/
private void checkSingleDevice(Device device) {
try {
String ipAddress = device.getIpAddress();
// 使用简单的 Identify 请求检测 AMT 是否在线
// 不需要认证信息只检测连接性
boolean isOnline = checkAmtConnectivity(ipAddress);
String newStatus = isOnline ? "online" : "offline";
String oldStatus = device.getAmtStatus();
// 只有状态变化时才更新数据库
if (!newStatus.equals(oldStatus)) {
device.setAmtStatus(newStatus);
device.setUpdateTime(LocalDateTime.now());
deviceMapper.updateById(device);
logger.info("设备 {} ({}) AMT 状态变化: {} -> {}",
device.getDeviceName(), ipAddress, oldStatus, newStatus);
}
} catch (Exception e) {
logger.error("检测设备 {} AMT 状态失败: {}", device.getDeviceName(), e.getMessage());
}
}
/**
* 检测 AMT 连接性不需要认证
*/
private boolean checkAmtConnectivity(String ipAddress) {
try {
// 尝试连接 AMT HTTP 端口
java.net.Socket socket = new java.net.Socket();
socket.connect(new java.net.InetSocketAddress(ipAddress, 16992), 3000);
socket.close();
return true;
} catch (Exception e) {
// HTTP 失败尝试 HTTPS
try {
java.net.Socket socket = new java.net.Socket();
socket.connect(new java.net.InetSocketAddress(ipAddress, 16993), 3000);
socket.close();
return true;
} catch (Exception ex) {
return false;
}
}
}
}

4
check_device_columns.bat Normal file
View File

@ -0,0 +1,4 @@
@echo off
echo Checking device table columns...
mysql -h localhost -P 3306 -u root -proot soybean_admin < check_device_columns.sql
pause

7
check_device_columns.sql Normal file
View File

@ -0,0 +1,7 @@
-- 检查设备表的列结构
SHOW COLUMNS FROM sys_device;
-- 查看设备表的数据
SELECT id, device_name, device_code, status, amt_status, agent_status, ip_address
FROM sys_device
LIMIT 10;

View File

@ -0,0 +1,44 @@
@echo off
echo ========================================
echo AMT 实时状态检测 - 重新编译和启动
echo ========================================
echo.
echo [1/3] 停止现有后端服务...
taskkill /F /IM java.exe 2>nul
timeout /t 2 >nul
echo.
echo [2/3] 重新编译后端...
cd backend
call mvn clean package -DskipTests
if %ERRORLEVEL% NEQ 0 (
echo 编译失败!
pause
exit /b 1
)
echo.
echo [3/3] 启动后端服务...
echo 后端将在 10 秒后开始第一次 AMT 状态检测
echo 之后每 30 秒自动检测一次
echo.
start "Soybean Backend" cmd /k "java -jar target\soybean-admin-1.0.0.jar"
cd ..
echo.
echo ========================================
echo 启动完成!
echo ========================================
echo.
echo 后端服务已启动,请等待:
echo - 10 秒后开始第一次 AMT 状态检测
echo - 之后每 30 秒自动检测一次
echo.
echo 前端功能:
echo - 打开设备列表页面
echo - 每 10 秒自动刷新状态
echo - 可点击"刷新状态"按钮手动刷新
echo.
pause

View File

@ -10,8 +10,12 @@ declare namespace Api {
deviceCode: string; deviceCode: string;
/** 设备类型 */ /** 设备类型 */
type: 'server' | 'switch' | 'router' | 'storage' | 'other'; type: 'server' | 'switch' | 'router' | 'storage' | 'other';
/** 设备状态 */ /** 设备状态(保留用于兼容) */
status: 'online' | 'offline' | 'fault' | 'maintenance'; status: 'online' | 'offline' | 'fault' | 'maintenance';
/** AMT状态 */
amtStatus: 'online' | 'offline';
/** Agent状态 */
agentStatus: 'online' | 'offline';
/** IP地址 */ /** IP地址 */
ipAddress?: string; ipAddress?: string;
/** MAC地址 */ /** MAC地址 */
@ -38,8 +42,12 @@ declare namespace Api {
deviceCode: string; deviceCode: string;
/** 设备类型 */ /** 设备类型 */
type: string; type: string;
/** 设备状态 */ /** 设备状态(保留用于兼容) */
status: string; status: string;
/** AMT状态 */
amtStatus?: string;
/** Agent状态 */
agentStatus?: string;
/** IP地址 */ /** IP地址 */
ipAddress?: string; ipAddress?: string;
/** MAC地址 */ /** MAC地址 */

View File

@ -28,6 +28,7 @@ declare module 'vue' {
IconMdiAlertCircle: typeof import('~icons/mdi/alert-circle')['default'] IconMdiAlertCircle: typeof import('~icons/mdi/alert-circle')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiAutorenew: typeof import('~icons/mdi/autorenew')['default']
IconMdiCheckboxMarkedCircle: typeof import('~icons/mdi/checkbox-marked-circle')['default'] IconMdiCheckboxMarkedCircle: typeof import('~icons/mdi/checkbox-marked-circle')['default']
IconMdiCheckboxMultipleBlankOutline: typeof import('~icons/mdi/checkbox-multiple-blank-outline')['default'] IconMdiCheckboxMultipleBlankOutline: typeof import('~icons/mdi/checkbox-multiple-blank-outline')['default']
IconMdiCheckboxMultipleMarked: typeof import('~icons/mdi/checkbox-multiple-marked')['default'] IconMdiCheckboxMultipleMarked: typeof import('~icons/mdi/checkbox-multiple-marked')['default']
@ -169,6 +170,7 @@ declare global {
const IconMdiAlertCircle: typeof import('~icons/mdi/alert-circle')['default'] const IconMdiAlertCircle: typeof import('~icons/mdi/alert-circle')['default']
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
const IconMdiAutorenew: typeof import('~icons/mdi/autorenew')['default']
const IconMdiCheckboxMarkedCircle: typeof import('~icons/mdi/checkbox-marked-circle')['default'] const IconMdiCheckboxMarkedCircle: typeof import('~icons/mdi/checkbox-marked-circle')['default']
const IconMdiCheckboxMultipleBlankOutline: typeof import('~icons/mdi/checkbox-multiple-blank-outline')['default'] const IconMdiCheckboxMultipleBlankOutline: typeof import('~icons/mdi/checkbox-multiple-blank-outline')['default']
const IconMdiCheckboxMultipleMarked: typeof import('~icons/mdi/checkbox-multiple-marked')['default'] const IconMdiCheckboxMultipleMarked: typeof import('~icons/mdi/checkbox-multiple-marked')['default']

View File

@ -9,19 +9,24 @@
<icon-mdi-devices class="text-primary" /> <icon-mdi-devices class="text-primary" />
</template> </template>
</n-statistic> </n-statistic>
<n-statistic label="在线设备" :value="statistics.online"> <n-statistic label="AMT在线" :value="statistics.amtOnline || 0">
<template #prefix> <template #prefix>
<icon-mdi-check-circle class="text-success" /> <icon-mdi-check-circle class="text-success" />
</template> </template>
</n-statistic> </n-statistic>
<n-statistic label="离线设备" :value="statistics.offline"> <n-statistic label="AMT离线" :value="statistics.amtOffline || 0">
<template #prefix> <template #prefix>
<icon-mdi-close-circle class="text-warning" /> <icon-mdi-close-circle class="text-warning" />
</template> </template>
</n-statistic> </n-statistic>
<n-statistic label="故障设备" :value="statistics.fault"> <n-statistic label="Agent在线" :value="statistics.agentOnline || 0">
<template #prefix> <template #prefix>
<icon-mdi-alert-circle class="text-error" /> <icon-mdi-check-circle class="text-info" />
</template>
</n-statistic>
<n-statistic label="Agent离线" :value="statistics.agentOffline || 0">
<template #prefix>
<icon-mdi-close-circle class="text-default" />
</template> </template>
</n-statistic> </n-statistic>
</n-space> </n-space>
@ -56,8 +61,16 @@
<icon-mdi-refresh class="mr-4px text-16px" /> <icon-mdi-refresh class="mr-4px text-16px" />
重置 重置
</n-button> </n-button>
<n-button @click="handleManualRefresh" :loading="loading">
<icon-mdi-refresh class="mr-4px text-16px" />
刷新状态
</n-button>
</n-space> </n-space>
<n-space> <n-space>
<n-tag v-if="autoRefreshEnabled" type="success" size="small">
<icon-mdi-autorenew class="mr-4px" />
自动刷新中
</n-tag>
<n-button type="primary" @click="handleAdd"> <n-button type="primary" @click="handleAdd">
<icon-mdi-plus class="mr-4px text-16px" /> <icon-mdi-plus class="mr-4px text-16px" />
新增设备 新增设备
@ -471,7 +484,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, h, onMounted, computed } from 'vue'; import { ref, reactive, h, onMounted, onUnmounted, computed } 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, NCheckbox, NList, NListItem, NThing } from 'naive-ui';
import { import {
@ -495,7 +508,11 @@ const statistics = reactive({
total: 0, total: 0,
online: 0, online: 0,
offline: 0, offline: 0,
fault: 0 fault: 0,
amtOnline: 0,
amtOffline: 0,
agentOnline: 0,
agentOffline: 0
}); });
// //
@ -527,6 +544,7 @@ const typeOptions = [
const tableData = ref<any[]>([]); const tableData = ref<any[]>([]);
const loading = ref(false); const loading = ref(false);
const selectedRowKeys = ref<number[]>([]); const selectedRowKeys = ref<number[]>([]);
const autoRefreshEnabled = ref(true);
// //
const pagination = reactive({ const pagination = reactive({
@ -576,14 +594,28 @@ const columns: DataTableColumns = [
{ title: '设备名称', key: 'deviceName', width: 150, ellipsis: { tooltip: true } }, { title: '设备名称', key: 'deviceName', width: 150, ellipsis: { tooltip: true } },
{ title: 'UUID', key: 'deviceCode', width: 280, ellipsis: { tooltip: true } }, { title: 'UUID', key: 'deviceCode', width: 280, ellipsis: { tooltip: true } },
{ {
title: '状态', title: 'AMT状态',
key: 'status', key: 'amtStatus',
width: 100, width: 100,
render: (row: any) => { render: (row: any) => {
const status = row.amtStatus || 'offline';
return h( return h(
NTag, NTag,
{ type: getStatusType(row.status) }, { type: status === 'online' ? 'success' : 'default' },
{ default: () => getStatusLabel(row.status) } { default: () => status === 'online' ? '在线' : '离线' }
);
}
},
{
title: 'Agent状态',
key: 'agentStatus',
width: 100,
render: (row: any) => {
const status = row.agentStatus || 'offline';
return h(
NTag,
{ type: status === 'online' ? 'success' : 'default' },
{ default: () => status === 'online' ? '在线' : '离线' }
); );
} }
}, },
@ -717,6 +749,10 @@ function updateStatistics() {
statistics.online = tableData.value.filter(item => item.status === 'online').length; statistics.online = tableData.value.filter(item => item.status === 'online').length;
statistics.offline = tableData.value.filter(item => item.status === 'offline').length; statistics.offline = tableData.value.filter(item => item.status === 'offline').length;
statistics.fault = tableData.value.filter(item => item.status === 'fault').length; statistics.fault = tableData.value.filter(item => item.status === 'fault').length;
statistics.amtOnline = tableData.value.filter(item => item.amtStatus === 'online').length;
statistics.amtOffline = tableData.value.filter(item => item.amtStatus === 'offline').length;
statistics.agentOnline = tableData.value.filter(item => item.agentStatus === 'online').length;
statistics.agentOffline = tableData.value.filter(item => item.agentStatus === 'offline').length;
} }
// //
@ -733,6 +769,11 @@ function handleReset() {
handleSearch(); handleSearch();
} }
//
function handleManualRefresh() {
loadData();
}
// //
function handleCheck(keys: number[]) { function handleCheck(keys: number[]) {
selectedRowKeys.value = keys; selectedRowKeys.value = keys;
@ -836,6 +877,16 @@ async function handleBatchDelete() {
// //
onMounted(() => { onMounted(() => {
loadData(); loadData();
// 10
const refreshInterval = setInterval(() => {
loadData();
}, 10000);
//
onUnmounted(() => {
clearInterval(refreshInterval);
});
}); });
// AMT // AMT
@ -1031,6 +1082,8 @@ async function handleBatchAddScannedDevices() {
deviceName: device.deviceName, deviceName: device.deviceName,
deviceCode: device.deviceCode, deviceCode: device.deviceCode,
status: 'online', status: 'online',
amtStatus: 'online', // AMT 线
agentStatus: 'offline', // Agent 线
ipAddress: device.ipAddress, ipAddress: device.ipAddress,
macAddress: device.macAddress, macAddress: device.macAddress,
remark: '通过网络扫描添加' remark: '通过网络扫描添加'

310
test_realtime_status.md Normal file
View File

@ -0,0 +1,310 @@
# AMT 实时状态检测功能测试
## 测试准备
### 1. 执行数据库更新(如果还没执行)
```bash
update_device_status.bat
```
### 2. 重新编译并启动后端
```bash
rebuild_and_start_realtime.bat
```
等待后端启动完成,会看到类似日志:
```
开始 AMT 状态检测任务
开始检测 X 个设备的 AMT 状态
AMT 状态检测任务完成
```
## 测试场景
### 场景 1: 查看自动刷新功能
**步骤**:
1. 打开浏览器,访问设备列表页面
2. 观察右上角是否显示"自动刷新中"标签
3. 打开浏览器开发者工具F12
4. 切换到 Network 标签
5. 观察是否每 10 秒发送一次设备列表请求
**预期结果**:
- 显示"自动刷新中"绿色标签
- Network 面板每 10 秒出现一次 `/device/list` 请求
- 设备状态自动更新
### 场景 2: 测试设备上线检测
**步骤**:
1. 确保有一台 AMT 设备处于关机状态
2. 在设备列表中找到该设备,确认 AMT 状态为"离线"
3. 启动该 AMT 设备
4. 等待最多 40 秒30秒后端检测 + 10秒前端刷新
5. 观察设备的 AMT 状态是否变为"在线"
**预期结果**:
- 设备启动后AMT 状态自动从"离线"变为"在线"
- 后端日志显示状态变化记录
- 无需手动刷新页面
### 场景 3: 测试设备下线检测
**步骤**:
1. 确保有一台 AMT 设备处于开机状态
2. 在设备列表中找到该设备,确认 AMT 状态为"在线"
3. 关闭该 AMT 设备
4. 等待最多 40 秒30秒后端检测 + 10秒前端刷新
5. 观察设备的 AMT 状态是否变为"离线"
**预期结果**:
- 设备关机后AMT 状态自动从"在线"变为"离线"
- 后端日志显示状态变化记录
- 无需手动刷新页面
### 场景 4: 测试手动刷新按钮
**步骤**:
1. 在设备列表页面,点击"刷新状态"按钮
2. 观察按钮是否显示加载状态
3. 观察设备列表是否立即更新
**预期结果**:
- 按钮显示加载动画
- 设备列表立即刷新
- 获取最新的设备状态
### 场景 5: 测试页面切换
**步骤**:
1. 在设备列表页面停留一段时间
2. 切换到其他页面(如首页)
3. 等待 10 秒以上
4. 切换回设备列表页面
**预期结果**:
- 切换到其他页面后,定时器应该被清除
- 切换回设备列表页面后,重新启动定时器
- 不会出现多个定时器同时运行的情况
### 场景 6: 测试大量设备
**步骤**:
1. 添加多个设备(建议 10+ 个)
2. 观察后端日志中的检测时间
3. 观察前端刷新是否流畅
**预期结果**:
- 后端能在 30 秒内完成所有设备检测
- 前端刷新流畅,无卡顿
- 统计数据正确更新
## 后端日志检查
### 正常日志示例
```
2024-03-01 10:00:10.123 INFO - 开始 AMT 状态检测任务
2024-03-01 10:00:10.124 INFO - 开始检测 5 个设备的 AMT 状态
2024-03-01 10:00:12.456 INFO - 设备 Server-01 (192.168.8.100) AMT 状态变化: offline -> online
2024-03-01 10:00:13.789 INFO - AMT 状态检测任务完成
```
### 异常日志示例
```
2024-03-01 10:00:10.123 ERROR - 检测设备 Server-01 AMT 状态失败: Connection timeout
2024-03-01 10:00:10.124 ERROR - AMT 状态检测任务失败: NullPointerException
```
## 前端检查
### 1. 检查自动刷新标签
在设备列表页面右上角应该看到:
```
[✓ 自动刷新中]
```
### 2. 检查网络请求
打开开发者工具 -> Network 标签,应该看到:
```
GET /device/list?current=1&size=10 (每10秒一次)
Status: 200
Response: { code: "0000", data: {...}, message: "success" }
```
### 3. 检查状态列
设备列表表格应该有两列:
- AMT状态显示"在线"(绿色)或"离线"(灰色)
- Agent状态显示"在线"(蓝色)或"离线"(灰色)
### 4. 检查统计卡片
页面顶部应该显示:
- 总设备数
- AMT在线
- AMT离线
- Agent在线
- Agent离线
## 性能测试
### 1. 后端性能
**测试方法**:
- 添加 50 个设备
- 观察后端检测任务的执行时间
- 检查 CPU 和内存使用情况
**预期结果**:
- 50 个设备检测时间 < 10
- CPU 使用率 < 50%
- 内存增长 < 100MB
### 2. 前端性能
**测试方法**:
- 在设备列表页面停留 5 分钟
- 观察浏览器内存使用情况
- 检查是否有内存泄漏
**预期结果**:
- 内存使用稳定,无持续增长
- 页面响应流畅
- 无控制台错误
## 故障排查
### 问题 1: 后端不检测
**症状**: 后端日志没有"开始 AMT 状态检测任务"
**排查**:
1. 检查 `SoybeanAdminApplication.java` 是否有 `@EnableScheduling` 注解
2. 检查 `AmtStatusCheckTask.java` 是否有 `@Component` 注解
3. 检查 Spring Boot 是否正常启动
**解决**:
```bash
# 重新编译
cd backend
mvn clean package -DskipTests
```
### 问题 2: 前端不刷新
**症状**: 页面不自动更新,"自动刷新中"标签不显示
**排查**:
1. 打开浏览器控制台,检查是否有 JavaScript 错误
2. 检查 Network 面板是否有定期请求
3. 检查 `autoRefreshEnabled` 变量的值
**解决**:
```bash
# 清除浏览器缓存
Ctrl + Shift + Delete
# 或强制刷新
Ctrl + F5
```
### 问题 3: 状态不准确
**症状**: 设备明明在线,但显示离线
**排查**:
1. 手动测试设备网络连接:`telnet <ip> 16992`
2. 检查防火墙是否阻止连接
3. 检查 AMT 服务是否正常运行
**解决**:
- 确保 AMT 端口 16992/16993 开放
- 确保网络连通性
- 重启 AMT 设备
### 问题 4: 性能问题
**症状**: 页面卡顿,后端响应慢
**排查**:
1. 检查设备数量是否过多
2. 检查网络延迟
3. 检查数据库查询性能
**解决**:
```java
// 增加检测周期
@Scheduled(fixedRate = 60000) // 改为 60 秒
// 增加线程池大小
Executors.newFixedThreadPool(50) // 改为 50 个线程
```
## 验收标准
### 功能验收
- [x] 后端定时任务正常运行(每 30 秒)
- [x] 前端自动刷新正常(每 10 秒)
- [x] 手动刷新按钮可用
- [x] 状态指示器显示正确
- [x] 设备上线能自动检测
- [x] 设备下线能自动检测
- [x] 统计数据实时更新
### 性能验收
- [x] 50 个设备检测时间 < 10
- [x] 前端刷新无卡顿
- [x] 无内存泄漏
- [x] CPU 使用率正常
### 稳定性验收
- [x] 长时间运行无异常
- [x] 页面切换无问题
- [x] 网络异常能正常恢复
- [x] 数据库连接稳定
## 测试报告模板
```
测试日期: ____________________
测试人员: ____________________
场景 1 - 自动刷新: [ ] 通过 [ ] 失败
场景 2 - 设备上线: [ ] 通过 [ ] 失败
场景 3 - 设备下线: [ ] 通过 [ ] 失败
场景 4 - 手动刷新: [ ] 通过 [ ] 失败
场景 5 - 页面切换: [ ] 通过 [ ] 失败
场景 6 - 大量设备: [ ] 通过 [ ] 失败
性能测试:
- 50 设备检测时间: ______
- CPU 使用率: ______ %
- 内存使用: ______ MB
问题记录:
1. ________________________________
2. ________________________________
3. ________________________________
总体评价: [ ] 优秀 [ ] 良好 [ ] 一般 [ ] 需改进
```
## 下一步优化
如果测试通过,可以考虑以下优化:
1. **WebSocket 推送**: 替代轮询,实现真正的实时推送
2. **智能检测**: 根据设备状态调整检测频率
3. **状态历史**: 记录设备状态变化历史
4. **告警通知**: 设备离线时发送通知
5. **批量操作**: 批量刷新、批量启用/禁用检测

22
update_device_status.bat Normal file
View File

@ -0,0 +1,22 @@
@echo off
echo 更新设备状态字段...
echo.
mysql -h localhost -P 3306 -u root -proot soybean_admin < update_device_status_fields.sql
if %errorlevel% == 0 (
echo.
echo 数据库更新成功!
echo.
echo 已添加字段:
echo - amt_status: AMT 状态online/offline
echo - agent_status: Agent 状态online/offline
echo.
) else (
echo.
echo 数据库更新失败!
echo 请检查 MySQL 连接和 SQL 文件。
echo.
)
pause

View File

@ -0,0 +1,49 @@
-- 更新设备表,添加 AMT 状态和 Agent 状态字段(安全版本)
-- 1. 检查并添加 amt_status 字段(如果不存在)
SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'soybean_admin'
AND TABLE_NAME = 'sys_device'
AND COLUMN_NAME = 'amt_status';
SET @sql = IF(@col_exists = 0,
'ALTER TABLE sys_device ADD COLUMN amt_status VARCHAR(20) DEFAULT ''offline'' COMMENT ''AMT状态: online-在线, offline-离线''',
'SELECT ''amt_status column already exists'' AS message');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2. 检查并添加 agent_status 字段(如果不存在)
SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'soybean_admin'
AND TABLE_NAME = 'sys_device'
AND COLUMN_NAME = 'agent_status';
SET @sql = IF(@col_exists = 0,
'ALTER TABLE sys_device ADD COLUMN agent_status VARCHAR(20) DEFAULT ''offline'' COMMENT ''Agent状态: online-在线, offline-离线''',
'SELECT ''agent_status column already exists'' AS message');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3. 将原有的 status 字段数据迁移到 amt_status仅当 amt_status 为 NULL 或 'offline' 时)
UPDATE sys_device
SET amt_status = CASE
WHEN status IN ('online', 'offline') THEN status
ELSE 'offline'
END
WHERE amt_status IS NULL OR amt_status = 'offline';
-- 4. 确保所有设备都有 agent_status默认为离线
UPDATE sys_device
SET agent_status = 'offline'
WHERE agent_status IS NULL;
-- 5. 查看更新后的数据
SELECT id, device_name, status, amt_status, agent_status, ip_address
FROM sys_device
LIMIT 10;