update system status api, add UPS/Docker status api

This commit is contained in:
Timi
2026-04-07 20:12:52 +08:00
parent cd7bc31e6b
commit b6a58b7376
38 changed files with 3721 additions and 281 deletions

View File

@@ -0,0 +1,142 @@
package com.imyeyu.api.modules.system.bean;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Docker 容器状态缓存
*
* @author Codex
* @since 2026-04-06
*/
@Data
@Component
public class DockerStatusStore {
/** 容器状态映射 */
private final Map<String, Container> containers = new LinkedHashMap<>();
/**
* 容器缓存
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Container {
/** 容器 ID */
private String id;
/** 容器名称 */
private String name;
/** 镜像 */
private String image;
/** 镜像 ID */
private String imageId;
/** 创建时间 */
private long createdAt;
/** 运行状态 */
private String state;
/** 状态描述 */
private String status;
/** 健康状态 */
private String healthStatus;
/** 启动时间 */
private String startedAt;
/** 结束时间 */
private String finishedAt;
/** 退出码 */
private Integer exitCode;
/** 重启次数 */
private int restartCount;
/** OOM 标记 */
private boolean oomKilled;
/** CPU 百分比 */
private Double cpuPercent;
/** 内存使用量 */
private Long memoryUsageBytes;
/** 内存限制 */
private Long memoryLimitBytes;
/** 内存百分比 */
private Double memoryPercent;
/** 网络接收字节 */
private Long networkRxBytes;
/** 网络发送字节 */
private Long networkTxBytes;
/** 块设备读取字节 */
private Long blockReadBytes;
/** 块设备写入字节 */
private Long blockWriteBytes;
/** 进程数 */
private Integer pids;
/** 采样时间 */
private long updatedAt;
/** 历史点 */
private final Deque<Point> history = new ArrayDeque<>();
}
/**
* 容器指标历史点
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Point {
/** 时间 */
private long at;
/** CPU 百分比 */
private Double cpuPercent;
/** 内存使用量 */
private Long memoryUsageBytes;
/** 内存百分比 */
private Double memoryPercent;
/** 网络接收字节 */
private Long networkRxBytes;
/** 网络发送字节 */
private Long networkTxBytes;
/** 块设备读取字节 */
private Long blockReadBytes;
/** 块设备写入字节 */
private Long blockWriteBytes;
/** 进程数 */
private Integer pids;
}
}

View File

@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.system.bean;
import lombok.Data;
import com.imyeyu.java.TimiJava;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.util.ArrayDeque;
@@ -11,22 +11,24 @@ import java.util.LinkedList;
import java.util.List;
/**
* 服务状态数据,所有动态数据左出右进,此对象由 IOC 托管
* 服务状态缓存
*
* <p>该对象只用于采集任务内部缓存,不直接作为接口协议返回。</p>
*
* @author 夜雨
* @version 2022-01-31 15:35
* @since 2022-01-31 15:35
*/
@Data
@Component
public class ServerStatus implements TimiJava {
/** 动态数据更新时轴 */
/** 采样时间轴 */
private LinkedList<Number> updateAxis = new LinkedList<>();
/** 系统 */
/** 操作系统 */
private OS os = new OS();
/** CPU 使用率 */
/** CPU */
private CPU cpu = new CPU();
/** 系统内存 */
@@ -35,17 +37,20 @@ public class ServerStatus implements TimiJava {
/** 网络 */
private Network network = new Network();
/** 本程序状态 */
/** 硬件 */
private Hardware hardware = new Hardware();
/** JVM */
private JVM jvm = new JVM();
/** 磁盘 */
private List<Partition> partitions = new ArrayList<>();
/** 存储分区 */
private List<StoragePartition> storagePartitions = new ArrayList<>();
/**
* 系统
* 操作系统
*
* @author 夜雨
* @version 2022-08-12 20:55
* @since 2022-08-12 20:55
*/
@Data
public static class OS {
@@ -58,10 +63,10 @@ public class ServerStatus implements TimiJava {
}
/**
* 虚拟机状态
* JVM
*
* @author 夜雨
* @version 2022-01-31 21:10
* @since 2022-01-31 21:10
*/
@Data
public static class JVM {
@@ -69,80 +74,83 @@ public class ServerStatus implements TimiJava {
/** 启动时间 */
private long bootAt;
/** JVM 名称 */
/** 名称 */
private String name;
/** JVM 版本 */
/** 版本 */
private String version;
/** 内存 */
/** GC 名称 */
private String gcName;
/** 堆内存 */
private Memory memory = new Memory();
/** 内存回收 */
private ZGC zgc = new ZGC();
/** GC 状态 */
private GC gc = new GC();
/**
* 内存
* JVM 内存
*
* @author 夜雨
* @version 2022-08-12 20:32
* @since 2022-08-12 20:32
*/
@Data
public static class Memory {
/** 初始 */
/** 初始大小 */
private long init;
/** 最大 */
/** 最大大小 */
private long max;
/** 已使用 */
/** 已用大小 */
private final Deque<Number> used = new ArrayDeque<>();
/** 已提交 */
/** 已提交大小 */
private final Deque<Number> committed = new ArrayDeque<>();
}
/**
* 内存回收
* GC 状态
*
* @author 夜雨
* @version 2022-08-12 20:32
* @since 2022-08-12 20:32
*/
@Data
public static class ZGC {
public static class GC {
/** 并发周期 */
private long syncCycles = 0;
/** 周期次数 */
private long syncCycles;
/** 累计并发周期耗时(毫秒) */
private long syncCyclesTimeTotal = 0;
/** 周期累计耗时 */
private long syncCyclesTimeTotal;
/** 累计次数 */
private long pauses = 0;
/** 暂停次数 */
private long pauses;
/** 累计回收暂停时长(毫秒) */
private long pausesTimeTotal = 0;
/** 暂停累计耗时 */
private long pausesTimeTotal;
/** 上一次回收时间 */
private long lastPauseAt = 0;
/** 上次暂停时间 */
private long lastPauseAt;
/** 上次回收大小 */
private long lastRecoverySize = 0;
/** 上次回收大小 */
private long lastRecoverySize;
/** 并发周期耗时 */
/** 周期耗时序列 */
private final Deque<Number> syncCyclesTime = new ArrayDeque<>();
/** 回收暂停时长 */
/** 暂停耗时序列 */
private final Deque<Number> pausesTime = new ArrayDeque<>();
}
}
/**
* 中央处理器
* CPU
*
* @author 夜雨
* @version 2022-01-31 15:40
* @since 2022-01-31 15:40
*/
@Data
public static class CPU {
@@ -150,19 +158,19 @@ public class ServerStatus implements TimiJava {
/** 名称 */
private String name;
/** 物理核心数 */
/** 物理核心数 */
private int coreCount;
/** 线程数量 */
/** 逻辑核心数 */
private int logicalCount;
/** 温度 */
private double temperature;
/** 系统使用 */
/** 系统用 */
private final Deque<Number> system = new ArrayDeque<>();
/** 总共已使用 */
/** 总用 */
private final Deque<Number> used = new ArrayDeque<>();
}
@@ -170,7 +178,7 @@ public class ServerStatus implements TimiJava {
* 系统内存
*
* @author 夜雨
* @version 2022-01-31 15:50
* @since 2022-01-31 15:50
*/
@Data
public static class Memory {
@@ -178,69 +186,174 @@ public class ServerStatus implements TimiJava {
/** 物理内存大小 */
private long size;
/** 交换区大小 */
/** 交换区大小 */
private long swapSize;
/** 已使用 */
/** 已用内存 */
private final Deque<Number> used = new ArrayDeque<>();
/** 交换区已使用 */
/** 已用交换分区 */
private final Deque<Number> swapUsed = new ArrayDeque<>();
}
/**
* 网卡网速
* 网
*
* @author 夜雨
* @version 2022-08-10 21:41
* @since 2022-08-10 21:41
*/
@Data
public static class Network {
/** 网卡名称 */
private String name;
/** 累计接收 */
private long recvTotal;
/** 累计发送 */
private long sentTotal;
/** 实时接收速 */
/** 实时接收速 */
private long recvNow;
/** 实时发送速 */
/** 实时发送速 */
private long sentNow;
/** MAC 地址 */
private String mac;
/** 发送 */
/** 接收包总数 */
private long recvPacketsTotal;
/** 发送包总数 */
private long sentPacketsTotal;
/** 输入错误包总数 */
private long inErrors;
/** 输出错误包总数 */
private long outErrors;
/** 输入丢弃包总数 */
private long inDrops;
/** 碰撞总数 */
private long collisions;
/** 发送序列 */
private final Deque<Number> sent = new ArrayDeque<>();
/** 接收 */
/** 接收序列 */
private final Deque<Number> recv = new ArrayDeque<>();
}
/**
* 分区
* 硬件信息
*
* @author 夜雨
* @version 2022-01-31 20:19
* @since 2026-04-06
*/
@Data
public static class Partition {
public static class Hardware {
/** 识别 UUID */
/** 风扇转速 */
private List<Integer> fanSpeeds = new ArrayList<>();
/** 主板信息 */
private Baseboard baseboard = new Baseboard();
/** BIOS 信息 */
private Firmware firmware = new Firmware();
}
/**
* 主板信息
*
* @author 夜雨
* @since 2026-04-06
*/
@Data
public static class Baseboard {
/** 厂商 */
private String manufacturer;
/** 型号 */
private String model;
/** 版本 */
private String version;
/** 序列号 */
private String serialNumber;
}
/**
* BIOS 信息
*
* @author 夜雨
* @since 2026-04-06
*/
@Data
public static class Firmware {
/** 厂商 */
private String manufacturer;
/** 名称 */
private String name;
/** 描述 */
private String description;
/** 版本 */
private String version;
/** 发布时间 */
private String releaseDate;
}
/**
* 存储分区
*
* @author 夜雨
* @since 2026-04-06
*/
@Data
public static class StoragePartition {
/** 物理磁盘名称 */
private String diskName;
/** 物理磁盘型号 */
private String diskModel;
/** 物理磁盘序列号 */
private String diskSerial;
/** 分区标识 */
private String partitionId;
/** 分区名称 */
private String partitionName;
/** 分区类型 */
private String partitionType;
/** 分区 UUID */
private String uuid;
/** 路径 */
private String path;
/** 挂载点 */
private String mountPoint;
/** 文件系统类型 */
private String type;
/** 分区总空间 */
private long totalBytes;
/** 已使用 */
private long used;
/** 分区已用空间 */
private Long usedBytes;
/** 总大小 */
private long total;
/** 磁盘传输耗时 */
private long transferTimeMs;
}
}

View File

@@ -0,0 +1,169 @@
package com.imyeyu.api.modules.system.bean;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/**
* UPS 状态缓存
*
* @author Codex
* @since 2026-04-07
*/
@Data
@Component
public class UpsStatusStore {
/** 当前状态 */
private Snapshot current;
/** 历史点 */
private final Deque<Point> history = new ArrayDeque<>();
/**
* UPS 当前快照
*
* @author Codex
* @since 2026-04-07
*/
@Data
public static class Snapshot {
/** 采样时间 */
private long updatedAt;
/** UPS 数据时间 */
private Long upsTime;
/** 上游主机地址 */
private String hostName;
/** 厂商名称 */
private String customer;
/** 上游版本 */
private String version;
/** 设备标识 */
private String deviceId;
/** UPS 类型 */
private String upsType;
/** UPS 形态 */
private String morphological;
/** 输入输出相位 */
private String ioPhase;
/** 工作模式 */
private String workMode;
/** 输入电压 */
private Double inputVoltage;
/** 输入频率 */
private Double inputFrequency;
/** 输出电压 */
private Double outputVoltage;
/** 输出频率 */
private Double outputFrequency;
/** 输出负载百分比 */
private Integer outputLoadPercent;
/** 电池电压 */
private Double batteryVoltage;
/** 电池容量百分比 */
private Integer batteryCapacity;
/** 电池剩余时间 */
private Integer batteryRemainTime;
/** 温度 */
private Double temperature;
/** 是否旁路运行 */
private boolean bypassActive;
/** 是否关机中 */
private boolean shutdownActive;
/** 是否输出开启 */
private boolean outputOn;
/** 是否充电中 */
private boolean charging;
/** 故障类型 */
private String faultType;
/** 故障明细 */
private String faultKind;
/** 告警列表 */
private List<String> warnings = new ArrayList<>();
}
/**
* UPS 历史点
*
* @author Codex
* @since 2026-04-07
*/
@Data
public static class Point {
/** 采样时间 */
private long at;
/** UPS 数据时间 */
private Long upsTime;
/** 工作模式 */
private String workMode;
/** 输入电压 */
private Double inputVoltage;
/** 输入频率 */
private Double inputFrequency;
/** 输出电压 */
private Double outputVoltage;
/** 输出频率 */
private Double outputFrequency;
/** 输出负载百分比 */
private Integer outputLoadPercent;
/** 电池电压 */
private Double batteryVoltage;
/** 电池容量百分比 */
private Integer batteryCapacity;
/** 电池剩余时间 */
private Integer batteryRemainTime;
/** 温度 */
private Double temperature;
/** 是否旁路运行 */
private boolean bypassActive;
/** 是否关机中 */
private boolean shutdownActive;
/** 是否输出开启 */
private boolean outputOn;
}
}

View File

@@ -0,0 +1,43 @@
package com.imyeyu.api.modules.system.controller;
import com.imyeyu.api.modules.system.service.DockerService;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* Docker 控制器
*
* @author Codex
* @since 2026-04-06
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/docker")
public class DockerController {
private final DockerService dockerService;
@GetMapping("/containers")
public List<DockerContainerSummaryView> listContainers() {
return dockerService.listContainers();
}
@GetMapping("/containers/{containerId}/status")
public DockerContainerStatusView getContainerStatus(@PathVariable String containerId) {
return dockerService.getContainerStatus(containerId);
}
@GetMapping("/containers/{containerId}/history")
public DockerContainerHistoryView getContainerHistory(@PathVariable String containerId, @RequestParam(required = false) String window) {
return dockerService.getContainerHistory(containerId, window);
}
}

View File

@@ -2,8 +2,10 @@ package com.imyeyu.api.modules.system.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.service.StatusService;
import com.imyeyu.api.modules.system.service.SystemService;
import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView;
import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView;
import com.imyeyu.api.modules.system.vo.TempAttachRequest;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.timi.TimiCode;
@@ -12,6 +14,7 @@ import com.imyeyu.spring.annotation.AOPLog;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -23,10 +26,10 @@ import java.io.IOException;
import java.util.concurrent.Semaphore;
/**
* 服务控制接口
* 服务控制
*
* @author 夜雨
* @version 2022-01-31 22:47
* @since 2022-01-31 22:47
*/
@Slf4j
@RestController
@@ -34,7 +37,7 @@ import java.util.concurrent.Semaphore;
@RequestMapping("/system/server")
public class SystemController {
private final ServerStatus serverStatus;
private final StatusService statusService;
private final SystemService service;
private final AttachmentService attachmentService;
@@ -42,16 +45,36 @@ public class SystemController {
private final Semaphore rebootSemaphore = new Semaphore(1);
private final Semaphore restoreSemaphore = new Semaphore(1);
/** @return 实时服务器状态 */
@RequestMapping("/status")
public ServerStatus getStatus() {
return serverStatus;
/**
* 获取系统状态快照
*
* @param metrics 返回指标,使用逗号分隔
* @return 状态快照
*/
@GetMapping("/status")
public SystemStatusSnapshotView getStatus(@RequestParam(required = false) String metrics) {
return statusService.getStatus(metrics);
}
/**
* 获取系统状态历史
*
* @param window 历史窗口,支持 s/m/h/d 后缀
* @param metrics 返回指标,使用逗号分隔
* @return 状态历史
*/
@GetMapping("/status/history")
public SystemStatusHistoryView getStatusHistory(
@RequestParam(required = false) String window,
@RequestParam(required = false) String metrics
) {
return statusService.getStatusHistory(window, metrics);
}
/**
* 更新系统
*
* @param file
* @param file 更新文件
*/
@AOPLog
@PostMapping("/update")
@@ -85,8 +108,7 @@ public class SystemController {
}
/**
* 停止系统
*
* 关闭系统
*/
@AOPLog
@RequestMapping("/shutdown")
@@ -111,7 +133,11 @@ public class SystemController {
}
}
// TODO 临时接口
/**
* 上传临时附件
*
* @param request 上传请求
*/
@AOPLog
@PostMapping("/attach")
public void uploadAttachment(TempAttachRequest request) {

View File

@@ -0,0 +1,34 @@
package com.imyeyu.api.modules.system.controller;
import com.imyeyu.api.modules.system.service.UpsService;
import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView;
import com.imyeyu.api.modules.system.vo.ups.UpsStatusView;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* UPS 控制器
*
* @author Codex
* @since 2026-04-07
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/ups")
public class UpsController {
private final UpsService upsService;
@GetMapping("/status")
public UpsStatusView getStatus() {
return upsService.getStatus();
}
@GetMapping("/history")
public UpsHistoryView getHistory(@RequestParam(required = false) String window) {
return upsService.getHistory(window);
}
}

View File

@@ -0,0 +1,40 @@
package com.imyeyu.api.modules.system.service;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView;
import java.util.List;
/**
* Docker 查询服务
*
* @author Codex
* @since 2026-04-06
*/
public interface DockerService {
/**
* 获取容器列表
*
* @return 容器列表
*/
List<DockerContainerSummaryView> listContainers();
/**
* 获取容器状态
*
* @param containerId 容器 ID
* @return 容器状态
*/
DockerContainerStatusView getContainerStatus(String containerId);
/**
* 获取容器历史
*
* @param containerId 容器 ID
* @param window 历史窗口
* @return 容器历史
*/
DockerContainerHistoryView getContainerHistory(String containerId, String window);
}

View File

@@ -0,0 +1,30 @@
package com.imyeyu.api.modules.system.service;
import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView;
import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView;
/**
* 系统状态查询服务
*
* @author Codex
* @since 2026-04-06
*/
public interface StatusService {
/**
* 获取当前状态快照
*
* @param metrics 返回指标
* @return 状态快照
*/
SystemStatusSnapshotView getStatus(String metrics);
/**
* 获取状态历史
*
* @param window 历史窗口
* @param metrics 返回指标
* @return 状态历史
*/
SystemStatusHistoryView getStatusHistory(String window, String metrics);
}

View File

@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.system.service;
import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView;
import com.imyeyu.api.modules.system.vo.ups.UpsStatusView;
/**
* UPS 查询服务
*
* @author Codex
* @since 2026-04-07
*/
public interface UpsService {
/**
* 获取 UPS 当前状态
*
* @return UPS 状态
*/
UpsStatusView getStatus();
/**
* 获取 UPS 历史
*
* @param window 历史窗口
* @return UPS 历史
*/
UpsHistoryView getHistory(String window);
}

View File

@@ -0,0 +1,171 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.system.bean.DockerStatusStore;
import com.imyeyu.api.modules.system.service.DockerService;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryPointView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* Docker 查询服务实现
*
* @author Codex
* @since 2026-04-06
*/
@Service
@RequiredArgsConstructor
public class DockerServiceImplement implements DockerService {
private final DockerStatusStore dockerStatusStore;
@Value("${docker.engine.collect-rate-ms:10000}")
private long collectRateMs;
@Override
public List<DockerContainerSummaryView> listContainers() {
synchronized (dockerStatusStore) {
List<DockerContainerSummaryView> result = new ArrayList<>(dockerStatusStore.getContainers().size());
for (DockerStatusStore.Container container : dockerStatusStore.getContainers().values()) {
DockerContainerSummaryView item = new DockerContainerSummaryView();
item.setId(container.getId());
item.setName(container.getName());
item.setImage(container.getImage());
item.setState(container.getState());
item.setStatus(container.getStatus());
item.setHealthStatus(container.getHealthStatus());
item.setCpuPercent(container.getCpuPercent());
item.setMemoryUsageBytes(container.getMemoryUsageBytes());
item.setMemoryLimitBytes(container.getMemoryLimitBytes());
item.setMemoryPercent(container.getMemoryPercent());
item.setNetworkRxBytes(container.getNetworkRxBytes());
item.setNetworkTxBytes(container.getNetworkTxBytes());
item.setUpdatedAt(container.getUpdatedAt());
result.add(item);
}
return result;
}
}
@Override
public DockerContainerStatusView getContainerStatus(String containerId) {
synchronized (dockerStatusStore) {
DockerStatusStore.Container container = findContainer(containerId);
if (container == null) {
return null;
}
DockerContainerStatusView view = new DockerContainerStatusView();
view.setId(container.getId());
view.setName(container.getName());
view.setImage(container.getImage());
view.setImageId(container.getImageId());
view.setCreatedAt(container.getCreatedAt());
view.setState(container.getState());
view.setStatus(container.getStatus());
view.setHealthStatus(container.getHealthStatus());
view.setStartedAt(container.getStartedAt());
view.setFinishedAt(container.getFinishedAt());
view.setExitCode(container.getExitCode());
view.setRestartCount(container.getRestartCount());
view.setOomKilled(container.isOomKilled());
view.setCpuPercent(container.getCpuPercent());
view.setMemoryUsageBytes(container.getMemoryUsageBytes());
view.setMemoryLimitBytes(container.getMemoryLimitBytes());
view.setMemoryPercent(container.getMemoryPercent());
view.setNetworkRxBytes(container.getNetworkRxBytes());
view.setNetworkTxBytes(container.getNetworkTxBytes());
view.setBlockReadBytes(container.getBlockReadBytes());
view.setBlockWriteBytes(container.getBlockWriteBytes());
view.setPids(container.getPids());
view.setUpdatedAt(container.getUpdatedAt());
return view;
}
}
@Override
public DockerContainerHistoryView getContainerHistory(String containerId, String window) {
synchronized (dockerStatusStore) {
DockerStatusStore.Container container = findContainer(containerId);
if (container == null) {
return null;
}
DockerContainerHistoryView view = new DockerContainerHistoryView();
view.setId(container.getId());
view.setName(container.getName());
view.setServerTime(Time.now());
view.setSampleRateMs(collectRateMs);
long windowMs = parseWindowMs(window);
long threshold = Time.now() - windowMs;
for (DockerStatusStore.Point point : container.getHistory()) {
if (point.getAt() < threshold) {
continue;
}
if (0 == view.getFrom()) {
view.setFrom(point.getAt());
}
view.setTo(point.getAt());
DockerContainerHistoryPointView item = new DockerContainerHistoryPointView();
item.setAt(point.getAt());
item.setCpuPercent(point.getCpuPercent());
item.setMemoryUsageBytes(point.getMemoryUsageBytes());
item.setMemoryPercent(point.getMemoryPercent());
item.setNetworkRxBytes(point.getNetworkRxBytes());
item.setNetworkTxBytes(point.getNetworkTxBytes());
item.setBlockReadBytes(point.getBlockReadBytes());
item.setBlockWriteBytes(point.getBlockWriteBytes());
item.setPids(point.getPids());
view.getPoints().add(item);
}
return view;
}
}
private long parseWindowMs(String window) {
if (window == null || window.isBlank()) {
return 60L * collectRateMs;
}
String normalized = window.trim().toLowerCase();
char suffix = normalized.charAt(normalized.length() - 1);
long unit = switch (suffix) {
case 's' -> 1000L;
case 'm' -> 60_000L;
case 'h' -> 3_600_000L;
case 'd' -> 86_400_000L;
default -> collectRateMs;
};
String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1);
try {
return Math.max(collectRateMs, Long.parseLong(valueText) * unit);
} catch (NumberFormatException e) {
return 60L * collectRateMs;
}
}
/**
* 按容器 ID 或前缀查找容器
*
* @param containerId 容器 ID
* @return 容器缓存
*/
private DockerStatusStore.Container findContainer(String containerId) {
DockerStatusStore.Container exact = dockerStatusStore.getContainers().get(containerId);
if (exact != null) {
return exact;
}
for (DockerStatusStore.Container container : dockerStatusStore.getContainers().values()) {
if (container.getId() != null && container.getId().startsWith(containerId)) {
return container;
}
}
return null;
}
}

View File

@@ -0,0 +1,414 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.service.StatusService;
import com.imyeyu.api.modules.system.vo.SystemStatusDataView;
import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView;
import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
/**
* 系统状态查询服务实现
*
* @author Codex
* @since 2026-04-06
*/
@Service
@RequiredArgsConstructor
public class StatusServiceImplement implements StatusService {
private final ServerStatus serverStatus;
private final SettingService settingService;
@Override
public SystemStatusSnapshotView getStatus(String metrics) {
long serverTime = Time.now();
int sampleRateMs = settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE);
EnumSet<Metric> selectedMetrics = parseMetrics(metrics);
synchronized (serverStatus) {
SystemStatusSnapshotView view = new SystemStatusSnapshotView();
view.setServerTime(serverTime);
view.setSampleRateMs(sampleRateMs);
view.setSnapshot(buildSnapshot(serverTime, selectedMetrics));
return view;
}
}
@Override
public SystemStatusHistoryView getStatusHistory(String window, String metrics) {
long serverTime = Time.now();
int sampleRateMs = settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE);
EnumSet<Metric> selectedMetrics = parseMetrics(metrics);
synchronized (serverStatus) {
SystemStatusHistoryView history = buildHistory(window, sampleRateMs, selectedMetrics);
history.setServerTime(serverTime);
history.setSampleRateMs(sampleRateMs);
return history;
}
}
/**
* 构建当前快照
*
* @param serverTime 服务端时间
* @param selectedMetrics 指标集合
* @return 当前快照
*/
private SystemStatusDataView.Snapshot buildSnapshot(long serverTime, EnumSet<Metric> selectedMetrics) {
SystemStatusDataView.Snapshot snapshot = new SystemStatusDataView.Snapshot();
if (selectedMetrics.contains(Metric.OS)) {
SystemStatusDataView.OS os = new SystemStatusDataView.OS();
os.setName(serverStatus.getOs().getName());
os.setBootAt(serverStatus.getOs().getBootAt());
os.setUptimeMs(Math.max(0, serverTime - serverStatus.getOs().getBootAt()));
snapshot.setOs(os);
}
if (selectedMetrics.contains(Metric.CPU)) {
SystemStatusDataView.CPU cpu = new SystemStatusDataView.CPU();
cpu.setModel(serverStatus.getCpu().getName());
cpu.setPhysicalCores(serverStatus.getCpu().getCoreCount());
cpu.setLogicalCores(serverStatus.getCpu().getLogicalCount());
cpu.setUsagePercent(lastDouble(serverStatus.getCpu().getUsed()));
cpu.setSystemPercent(lastDouble(serverStatus.getCpu().getSystem()));
cpu.setTemperatureCelsius(serverStatus.getCpu().getTemperature());
snapshot.setCpu(cpu);
}
if (selectedMetrics.contains(Metric.MEMORY)) {
SystemStatusDataView.Memory memory = new SystemStatusDataView.Memory();
Long usedBytes = lastLong(serverStatus.getMemory().getUsed());
Long swapUsedBytes = lastLong(serverStatus.getMemory().getSwapUsed());
memory.setTotalBytes(serverStatus.getMemory().getSize());
memory.setUsedBytes(usedBytes);
memory.setUsagePercent(toPercent(usedBytes, serverStatus.getMemory().getSize()));
memory.setSwapTotalBytes(serverStatus.getMemory().getSwapSize());
memory.setSwapUsedBytes(swapUsedBytes);
snapshot.setMemory(memory);
}
if (selectedMetrics.contains(Metric.JVM)) {
SystemStatusDataView.JVM jvm = new SystemStatusDataView.JVM();
SystemStatusDataView.GC gc = new SystemStatusDataView.GC();
jvm.setName(serverStatus.getJvm().getName());
jvm.setVersion(serverStatus.getJvm().getVersion());
jvm.setBootAt(serverStatus.getJvm().getBootAt());
jvm.setHeapInitBytes(serverStatus.getJvm().getMemory().getInit());
jvm.setHeapMaxBytes(serverStatus.getJvm().getMemory().getMax());
jvm.setHeapUsedBytes(lastLong(serverStatus.getJvm().getMemory().getUsed()));
jvm.setHeapCommittedBytes(lastLong(serverStatus.getJvm().getMemory().getCommitted()));
gc.setCollector(serverStatus.getJvm().getGcName());
gc.setCycleCount(serverStatus.getJvm().getGc().getSyncCycles());
gc.setPauseCount(serverStatus.getJvm().getGc().getPauses());
gc.setLastPauseAt(serverStatus.getJvm().getGc().getLastPauseAt());
gc.setLastRecoveredBytes(serverStatus.getJvm().getGc().getLastRecoverySize());
jvm.setGc(gc);
snapshot.setJvm(jvm);
}
if (selectedMetrics.contains(Metric.NETWORK)) {
SystemStatusDataView.Network network = new SystemStatusDataView.Network();
network.setInterfaceName(serverStatus.getNetwork().getName());
network.setMac(serverStatus.getNetwork().getMac());
network.setRxBytesPerSecond(serverStatus.getNetwork().getRecvNow());
network.setTxBytesPerSecond(serverStatus.getNetwork().getSentNow());
network.setRxTotalBytes(serverStatus.getNetwork().getRecvTotal());
network.setTxTotalBytes(serverStatus.getNetwork().getSentTotal());
network.setRxPacketsTotal(serverStatus.getNetwork().getRecvPacketsTotal());
network.setTxPacketsTotal(serverStatus.getNetwork().getSentPacketsTotal());
network.setInErrors(serverStatus.getNetwork().getInErrors());
network.setOutErrors(serverStatus.getNetwork().getOutErrors());
network.setInDrops(serverStatus.getNetwork().getInDrops());
network.setCollisions(serverStatus.getNetwork().getCollisions());
snapshot.setNetwork(network);
}
if (selectedMetrics.contains(Metric.HARDWARE)) {
SystemStatusDataView.Hardware hardware = new SystemStatusDataView.Hardware();
SystemStatusDataView.Baseboard baseboard = new SystemStatusDataView.Baseboard();
SystemStatusDataView.Firmware firmware = new SystemStatusDataView.Firmware();
hardware.setFanSpeeds(new ArrayList<>(serverStatus.getHardware().getFanSpeeds()));
baseboard.setManufacturer(serverStatus.getHardware().getBaseboard().getManufacturer());
baseboard.setModel(serverStatus.getHardware().getBaseboard().getModel());
baseboard.setVersion(serverStatus.getHardware().getBaseboard().getVersion());
baseboard.setSerialNumber(serverStatus.getHardware().getBaseboard().getSerialNumber());
firmware.setManufacturer(serverStatus.getHardware().getFirmware().getManufacturer());
firmware.setName(serverStatus.getHardware().getFirmware().getName());
firmware.setDescription(serverStatus.getHardware().getFirmware().getDescription());
firmware.setVersion(serverStatus.getHardware().getFirmware().getVersion());
firmware.setReleaseDate(serverStatus.getHardware().getFirmware().getReleaseDate());
hardware.setBaseboard(baseboard);
hardware.setFirmware(firmware);
snapshot.setHardware(hardware);
}
if (selectedMetrics.contains(Metric.STORAGE)) {
List<SystemStatusDataView.StoragePartition> storagePartitions = new ArrayList<>();
for (ServerStatus.StoragePartition partition : serverStatus.getStoragePartitions()) {
SystemStatusDataView.StoragePartition item = new SystemStatusDataView.StoragePartition();
item.setDiskName(partition.getDiskName());
item.setDiskModel(partition.getDiskModel());
item.setDiskSerial(partition.getDiskSerial());
item.setPartitionId(partition.getPartitionId());
item.setPartitionName(partition.getPartitionName());
item.setPartitionType(partition.getPartitionType());
item.setUuid(partition.getUuid());
item.setMountPoint(partition.getMountPoint());
item.setTotalBytes(partition.getTotalBytes());
item.setUsedBytes(partition.getUsedBytes());
item.setUsagePercent(toPercent(partition.getUsedBytes(), partition.getTotalBytes()));
item.setTransferTimeMs(partition.getTransferTimeMs());
storagePartitions.add(item);
}
snapshot.setStoragePartitions(storagePartitions);
}
return snapshot;
}
/**
* 构建历史数据
*
* @param window 历史窗口
* @param sampleRateMs 采样周期
* @param selectedMetrics 指标集合
* @return 历史数据
*/
private SystemStatusHistoryView buildHistory(String window, int sampleRateMs, EnumSet<Metric> selectedMetrics) {
SystemStatusHistoryView view = new SystemStatusHistoryView();
List<Long> axis = copyLongs(serverStatus.getUpdateAxis());
if (axis.isEmpty()) {
return view;
}
int startIndex = resolveStartIndex(axis, parseWindowMs(window, sampleRateMs));
view.setFrom(axis.get(startIndex));
view.setTo(axis.get(axis.size() - 1));
List<Double> cpuUsed = selectedMetrics.contains(Metric.CPU) ? copyDoubles(serverStatus.getCpu().getUsed()) : List.of();
List<Double> cpuSystem = selectedMetrics.contains(Metric.CPU) ? copyDoubles(serverStatus.getCpu().getSystem()) : List.of();
List<Long> memoryUsed = selectedMetrics.contains(Metric.MEMORY) ? copyLongs(serverStatus.getMemory().getUsed()) : List.of();
List<Long> swapUsed = selectedMetrics.contains(Metric.MEMORY) ? copyLongs(serverStatus.getMemory().getSwapUsed()) : List.of();
List<Long> heapUsed = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getMemory().getUsed()) : List.of();
List<Long> heapCommitted = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getMemory().getCommitted()) : List.of();
List<Long> gcCycleTime = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getGc().getSyncCyclesTime()) : List.of();
List<Long> gcPauseTime = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getGc().getPausesTime()) : List.of();
List<Long> rx = selectedMetrics.contains(Metric.NETWORK) ? toRate(copyLongs(serverStatus.getNetwork().getRecv()), sampleRateMs) : List.of();
List<Long> tx = selectedMetrics.contains(Metric.NETWORK) ? toRate(copyLongs(serverStatus.getNetwork().getSent()), sampleRateMs) : List.of();
for (int index = startIndex; index < axis.size(); index++) {
SystemStatusDataView.Point point = new SystemStatusDataView.Point();
point.setAt(axis.get(index));
if (selectedMetrics.contains(Metric.CPU)) {
point.setCpuUsagePercent(getAlignedValue(cpuUsed, axis.size(), index));
point.setCpuSystemPercent(getAlignedValue(cpuSystem, axis.size(), index));
}
if (selectedMetrics.contains(Metric.MEMORY)) {
point.setMemoryUsedBytes(getAlignedValue(memoryUsed, axis.size(), index));
point.setSwapUsedBytes(getAlignedValue(swapUsed, axis.size(), index));
}
if (selectedMetrics.contains(Metric.JVM)) {
point.setHeapUsedBytes(getAlignedValue(heapUsed, axis.size(), index));
point.setHeapCommittedBytes(getAlignedValue(heapCommitted, axis.size(), index));
point.setGcCycleTimeMs(getAlignedValue(gcCycleTime, axis.size(), index));
point.setGcPauseTimeMs(getAlignedValue(gcPauseTime, axis.size(), index));
}
if (selectedMetrics.contains(Metric.NETWORK)) {
point.setRxBytesPerSecond(getAlignedValue(rx, axis.size(), index));
point.setTxBytesPerSecond(getAlignedValue(tx, axis.size(), index));
}
view.getPoints().add(point);
}
return view;
}
/**
* 解析指标
*
* @param metrics 指标字符串
* @return 指标集合
*/
private EnumSet<Metric> parseMetrics(String metrics) {
if (metrics == null || metrics.isBlank()) {
return EnumSet.allOf(Metric.class);
}
EnumSet<Metric> selected = EnumSet.noneOf(Metric.class);
for (String metric : metrics.split(",")) {
switch (metric.trim().toLowerCase(Locale.ROOT)) {
case "os" -> selected.add(Metric.OS);
case "cpu" -> selected.add(Metric.CPU);
case "memory" -> selected.add(Metric.MEMORY);
case "jvm", "gc" -> selected.add(Metric.JVM);
case "network" -> selected.add(Metric.NETWORK);
case "storage", "disk", "disks" -> selected.add(Metric.STORAGE);
case "hardware", "board", "bios", "fan", "fans" -> selected.add(Metric.HARDWARE);
default -> {
}
}
}
return selected.isEmpty() ? EnumSet.allOf(Metric.class) : selected;
}
/**
* 解析窗口毫秒数
*
* @param window 窗口字符串
* @param sampleRateMs 采样周期
* @return 窗口毫秒数
*/
private long parseWindowMs(String window, int sampleRateMs) {
if (window == null || window.isBlank()) {
return (long) settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) * sampleRateMs;
}
String normalized = window.trim().toLowerCase(Locale.ROOT);
char suffix = normalized.charAt(normalized.length() - 1);
long unit = switch (suffix) {
case 's' -> 1000L;
case 'm' -> 60_000L;
case 'h' -> 3_600_000L;
case 'd' -> 86_400_000L;
default -> sampleRateMs;
};
String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1);
try {
return Math.max(sampleRateMs, Long.parseLong(valueText) * unit);
} catch (NumberFormatException e) {
return (long) settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) * sampleRateMs;
}
}
/**
* 解析历史起点
*
* @param axis 时间轴
* @param windowMs 窗口毫秒数
* @return 起点下标
*/
private int resolveStartIndex(List<Long> axis, long windowMs) {
long threshold = axis.get(axis.size() - 1) - windowMs;
for (int index = 0; index < axis.size(); index++) {
if (threshold <= axis.get(index)) {
return index;
}
}
return axis.size() - 1;
}
/**
* 将周期累计值转换为每秒速率
*
* @param source 原始列表
* @param sampleRateMs 采样周期
* @return 速率列表
*/
private List<Long> toRate(List<Long> source, int sampleRateMs) {
List<Long> result = new ArrayList<>(source.size());
for (Long value : source) {
result.add(value == null ? null : value * 1000 / sampleRateMs);
}
return result;
}
/**
* 获取与时间轴尾部对齐的值
*
* @param values 数据列表
* @param axisSize 时间轴长度
* @param axisIndex 时间轴下标
* @param <T> 数据类型
* @return 对齐后的值
*/
private <T> T getAlignedValue(List<T> values, int axisSize, int axisIndex) {
int valueIndex = axisIndex - (axisSize - values.size());
if (valueIndex < 0 || values.size() <= valueIndex) {
return null;
}
return values.get(valueIndex);
}
/**
* 复制长整型列表
*
* @param source 原始队列
* @return 列表
*/
private List<Long> copyLongs(Deque<Number> source) {
List<Long> result = new ArrayList<>(source.size());
for (Number number : source) {
result.add(number == null ? null : number.longValue());
}
return result;
}
/**
* 复制浮点列表
*
* @param source 原始队列
* @return 列表
*/
private List<Double> copyDoubles(Deque<Number> source) {
List<Double> result = new ArrayList<>(source.size());
for (Number number : source) {
result.add(number == null ? null : number.doubleValue());
}
return result;
}
/**
* 获取最后一个长整型值
*
* @param source 队列
* @return 值
*/
private Long lastLong(Deque<Number> source) {
Number number = source.peekLast();
return number == null ? null : number.longValue();
}
/**
* 获取最后一个浮点值
*
* @param source 队列
* @return 值
*/
private Double lastDouble(Deque<Number> source) {
Number number = source.peekLast();
return number == null ? null : number.doubleValue();
}
/**
* 计算百分比
*
* @param used 已用值
* @param total 总值
* @return 百分比
*/
private Double toPercent(Long used, long total) {
if (used == null || total <= 0) {
return null;
}
return used * 100D / total;
}
/**
* 状态指标
*
* @author Codex
* @since 2026-04-06
*/
private enum Metric {
OS,
CPU,
MEMORY,
JVM,
NETWORK,
HARDWARE,
STORAGE
}
}

View File

@@ -0,0 +1,151 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.system.bean.UpsStatusStore;
import com.imyeyu.api.modules.system.service.UpsService;
import com.imyeyu.api.modules.system.task.UpsStatusTask;
import com.imyeyu.api.modules.system.vo.ups.UpsHistoryPointView;
import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView;
import com.imyeyu.api.modules.system.vo.ups.UpsStatusView;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* UPS 查询服务实现
*
* @author Codex
* @since 2026-04-07
*/
@Service
@RequiredArgsConstructor
public class UpsServiceImplement implements UpsService {
private final UpsStatusTask upsStatusTask;
private final UpsStatusStore upsStatusStore;
@Value("${ups.collect-rate-ms:60000}")
private long collectRateMs;
@Override
public UpsStatusView getStatus() {
ensureCurrentSnapshot();
synchronized (upsStatusStore) {
UpsStatusStore.Snapshot snapshot = upsStatusStore.getCurrent();
if (snapshot == null) {
return null;
}
UpsStatusView view = new UpsStatusView();
view.setServerTime(System.currentTimeMillis());
view.setUpsTime(snapshot.getUpsTime());
view.setHostName(snapshot.getHostName());
view.setCustomer(snapshot.getCustomer());
view.setVersion(snapshot.getVersion());
view.setDeviceId(snapshot.getDeviceId());
view.setUpsType(snapshot.getUpsType());
view.setMorphological(snapshot.getMorphological());
view.setIoPhase(snapshot.getIoPhase());
view.setWorkMode(snapshot.getWorkMode());
view.setInputVoltage(snapshot.getInputVoltage());
view.setInputFrequency(snapshot.getInputFrequency());
view.setOutputVoltage(snapshot.getOutputVoltage());
view.setOutputFrequency(snapshot.getOutputFrequency());
view.setOutputLoadPercent(snapshot.getOutputLoadPercent());
view.setBatteryVoltage(snapshot.getBatteryVoltage());
view.setBatteryCapacity(snapshot.getBatteryCapacity());
view.setBatteryRemainTime(snapshot.getBatteryRemainTime());
view.setTemperature(snapshot.getTemperature());
view.setBypassActive(snapshot.isBypassActive());
view.setShutdownActive(snapshot.isShutdownActive());
view.setOutputOn(snapshot.isOutputOn());
view.setCharging(snapshot.isCharging());
view.setFaultType(snapshot.getFaultType());
view.setFaultKind(snapshot.getFaultKind());
view.setWarnings(snapshot.getWarnings());
return view;
}
}
@Override
public UpsHistoryView getHistory(String window) {
ensureCurrentSnapshot();
UpsHistoryView view = new UpsHistoryView();
view.setServerTime(System.currentTimeMillis());
view.setSampleRateMs(collectRateMs);
long windowMs = parseWindowMs(window);
long threshold = view.getServerTime() - windowMs;
synchronized (upsStatusStore) {
for (UpsStatusStore.Point point : upsStatusStore.getHistory()) {
if (point.getAt() < threshold) {
continue;
}
if (0 == view.getFrom()) {
view.setFrom(point.getAt());
}
view.setTo(point.getAt());
UpsHistoryPointView item = new UpsHistoryPointView();
item.setAt(point.getAt());
item.setUpsTime(point.getUpsTime());
item.setWorkMode(point.getWorkMode());
item.setInputVoltage(point.getInputVoltage());
item.setInputFrequency(point.getInputFrequency());
item.setOutputVoltage(point.getOutputVoltage());
item.setOutputFrequency(point.getOutputFrequency());
item.setOutputLoadPercent(point.getOutputLoadPercent());
item.setBatteryVoltage(point.getBatteryVoltage());
item.setBatteryCapacity(point.getBatteryCapacity());
item.setBatteryRemainTime(point.getBatteryRemainTime());
item.setTemperature(point.getTemperature());
item.setBypassActive(point.isBypassActive());
item.setShutdownActive(point.isShutdownActive());
item.setOutputOn(point.isOutputOn());
view.getPoints().add(item);
}
}
return view;
}
/**
* 保证当前快照存在
*/
private void ensureCurrentSnapshot() {
synchronized (upsStatusStore) {
if (upsStatusStore.getCurrent() != null) {
return;
}
}
upsStatusTask.collectOnce();
}
/**
* 解析历史窗口
*
* @param window 历史窗口
* @return 窗口毫秒数
*/
private long parseWindowMs(String window) {
long defaultWindowMs = 24L * 60 * 60 * 1000;
if (window == null || window.isBlank()) {
return defaultWindowMs;
}
String normalized = window.trim().toLowerCase();
char suffix = normalized.charAt(normalized.length() - 1);
long unit = switch (suffix) {
case 's' -> 1000L;
case 'm' -> 60_000L;
case 'h' -> 3_600_000L;
case 'd' -> 86_400_000L;
default -> collectRateMs;
};
String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1);
try {
long parsed = Long.parseLong(valueText) * unit;
long maxWindowMs = UpsStatusTask.MAX_HISTORY_MS;
return Math.max(collectRateMs, Math.min(parsed, maxWindowMs));
} catch (NumberFormatException e) {
return defaultWindowMs;
}
}
}

View File

@@ -0,0 +1,272 @@
package com.imyeyu.api.modules.system.task;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.imyeyu.api.modules.system.bean.DockerStatusStore;
import com.imyeyu.api.modules.system.util.DockerEngineClient;
import com.imyeyu.utils.Time;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Docker 状态采集任务
*
* @author Codex
* @since 2026-04-06
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DockerStatusTask implements SchedulingConfigurer {
private final DockerEngineClient dockerEngineClient;
private final DockerStatusStore dockerStatusStore;
@Value("${docker.engine.collect-enabled:true}")
private boolean collectEnabled;
@Value("${docker.engine.collect-rate-ms:10000}")
private long collectRateMs;
@Value("${docker.engine.history-limit:120}")
private int historyLimit;
@Override
public void configureTasks(@NotNull ScheduledTaskRegistrar taskRegistrar) {
if (!collectEnabled) {
return;
}
PeriodicTrigger trigger = new PeriodicTrigger(collectRateMs, TimeUnit.MILLISECONDS);
trigger.setInitialDelay(0);
taskRegistrar.addTriggerTask(this::collect, trigger);
}
private void collect() {
try {
JsonArray containers = dockerEngineClient.getJson("/containers/json", DockerEngineClient.query("all", "true")).getAsJsonArray();
long now = Time.now();
synchronized (dockerStatusStore) {
Set<String> activeIds = new HashSet<>();
for (JsonElement item : containers) {
try {
JsonObject summary = item.getAsJsonObject();
String containerId = getAsString(summary, "Id");
activeIds.add(containerId);
DockerStatusStore.Container container = dockerStatusStore.getContainers().computeIfAbsent(containerId, key -> new DockerStatusStore.Container());
updateContainerSummary(container, summary);
updateContainerInspect(containerId, container);
updateContainerStats(containerId, container, now);
} catch (Exception e) {
log.error("collect docker container item error", e);
}
}
dockerStatusStore.getContainers().entrySet().removeIf(item -> !activeIds.contains(item.getKey()));
}
} catch (Exception e) {
log.error("collect docker container status error", e);
}
}
private void updateContainerSummary(DockerStatusStore.Container container, JsonObject summary) {
container.setId(getAsString(summary, "Id"));
container.setName(trimContainerName(readFirstArrayText(summary, "Names")));
container.setImage(getAsString(summary, "Image"));
container.setImageId(getAsString(summary, "ImageID"));
container.setCreatedAt(getAsLong(summary, "Created") * 1000);
container.setState(getAsString(summary, "State"));
container.setStatus(getAsString(summary, "Status"));
}
private void updateContainerInspect(String containerId, DockerStatusStore.Container container) {
JsonObject inspect = dockerEngineClient.getJson("/containers/%s/json".formatted(containerId), Map.of()).getAsJsonObject();
JsonObject state = getAsObject(inspect, "State");
container.setStartedAt(getAsString(state, "StartedAt"));
container.setFinishedAt(getAsString(state, "FinishedAt"));
container.setExitCode(getAsInteger(state, "ExitCode"));
container.setRestartCount(getAsInteger(inspect, "RestartCount", 0));
container.setOomKilled(getAsBoolean(state, "OOMKilled"));
JsonObject health = getAsObject(state, "Health");
container.setHealthStatus(health == null ? null : getAsString(health, "Status"));
}
private void updateContainerStats(String containerId, DockerStatusStore.Container container, long now) {
JsonObject stats = dockerEngineClient.getJson("/containers/%s/stats".formatted(containerId), DockerEngineClient.query("stream", "false")).getAsJsonObject();
Double cpuPercent = calculateCpuPercent(stats);
Long memoryUsageBytes = getNestedLong(stats, "memory_stats", "usage");
Long memoryLimitBytes = getNestedLong(stats, "memory_stats", "limit");
Double memoryPercent = null;
if (memoryUsageBytes != null && memoryLimitBytes != null && 0 < memoryLimitBytes) {
memoryPercent = memoryUsageBytes * 100D / memoryLimitBytes;
}
Long networkRxBytes = 0L;
Long networkTxBytes = 0L;
JsonObject networks = getAsObject(stats, "networks");
if (networks != null) {
for (Map.Entry<String, JsonElement> item : networks.entrySet()) {
JsonObject network = item.getValue().getAsJsonObject();
networkRxBytes += getAsLong(network, "rx_bytes", 0L);
networkTxBytes += getAsLong(network, "tx_bytes", 0L);
}
}
Long blockReadBytes = 0L;
Long blockWriteBytes = 0L;
JsonObject blkioStats = getAsObject(stats, "blkio_stats");
JsonArray ioServiceBytes = blkioStats == null ? null : blkioStats.getAsJsonArray("io_service_bytes_recursive");
if (ioServiceBytes != null) {
for (JsonElement item : ioServiceBytes) {
JsonObject io = item.getAsJsonObject();
String op = getAsString(io, "op");
long value = getAsLong(io, "value", 0L);
if ("Read".equalsIgnoreCase(op)) {
blockReadBytes += value;
} else if ("Write".equalsIgnoreCase(op)) {
blockWriteBytes += value;
}
}
}
Integer pids = getNestedInteger(stats, "pids_stats", "current");
container.setCpuPercent(cpuPercent);
container.setMemoryUsageBytes(memoryUsageBytes);
container.setMemoryLimitBytes(memoryLimitBytes);
container.setMemoryPercent(memoryPercent);
container.setNetworkRxBytes(networkRxBytes);
container.setNetworkTxBytes(networkTxBytes);
container.setBlockReadBytes(blockReadBytes);
container.setBlockWriteBytes(blockWriteBytes);
container.setPids(pids);
container.setUpdatedAt(now);
DockerStatusStore.Point point = new DockerStatusStore.Point();
point.setAt(now);
point.setCpuPercent(cpuPercent);
point.setMemoryUsageBytes(memoryUsageBytes);
point.setMemoryPercent(memoryPercent);
point.setNetworkRxBytes(networkRxBytes);
point.setNetworkTxBytes(networkTxBytes);
point.setBlockReadBytes(blockReadBytes);
point.setBlockWriteBytes(blockWriteBytes);
point.setPids(pids);
container.getHistory().addLast(point);
while (historyLimit < container.getHistory().size()) {
container.getHistory().pollFirst();
}
}
private Double calculateCpuPercent(JsonObject stats) {
Long cpuTotal = getNestedLong(stats, "cpu_stats", "cpu_usage", "total_usage");
Long preCpuTotal = getNestedLong(stats, "precpu_stats", "cpu_usage", "total_usage");
Long systemTotal = getNestedLong(stats, "cpu_stats", "system_cpu_usage");
Long preSystemTotal = getNestedLong(stats, "precpu_stats", "system_cpu_usage");
Integer onlineCpus = getNestedInteger(stats, "cpu_stats", "online_cpus");
if (onlineCpus == null || onlineCpus <= 0) {
JsonArray perCpuUsage = getNestedArray(stats, "cpu_stats", "cpu_usage", "percpu_usage");
onlineCpus = perCpuUsage == null ? 1 : perCpuUsage.size();
}
if (cpuTotal == null || preCpuTotal == null || systemTotal == null || preSystemTotal == null) {
return null;
}
long cpuDelta = cpuTotal - preCpuTotal;
long systemDelta = systemTotal - preSystemTotal;
if (cpuDelta <= 0 || systemDelta <= 0) {
return 0D;
}
return cpuDelta * 100D * onlineCpus / systemDelta;
}
private JsonObject getAsObject(JsonObject source, String key) {
if (source == null || !source.has(key) || source.get(key).isJsonNull()) {
return null;
}
return source.getAsJsonObject(key);
}
private JsonArray getNestedArray(JsonObject source, String... keys) {
JsonElement current = source;
for (String key : keys) {
if (current == null || !current.isJsonObject() || !current.getAsJsonObject().has(key)) {
return null;
}
current = current.getAsJsonObject().get(key);
}
return current != null && current.isJsonArray() ? current.getAsJsonArray() : null;
}
private Long getNestedLong(JsonObject source, String... keys) {
JsonElement current = source;
for (String key : keys) {
if (current == null || !current.isJsonObject() || !current.getAsJsonObject().has(key)) {
return null;
}
current = current.getAsJsonObject().get(key);
}
return current == null || current.isJsonNull() ? null : current.getAsLong();
}
private Integer getNestedInteger(JsonObject source, String... keys) {
Long value = getNestedLong(source, keys);
return value == null ? null : value.intValue();
}
private String readFirstArrayText(JsonObject source, String key) {
if (source == null || !source.has(key) || !source.get(key).isJsonArray() || source.getAsJsonArray(key).isEmpty()) {
return null;
}
return source.getAsJsonArray(key).get(0).getAsString();
}
private String getAsString(JsonObject source, String key) {
if (source == null || !source.has(key) || source.get(key).isJsonNull()) {
return null;
}
return source.get(key).getAsString();
}
private long getAsLong(JsonObject source, String key) {
return getAsLong(source, key, 0L);
}
private long getAsLong(JsonObject source, String key, long defaultValue) {
if (source == null || !source.has(key) || source.get(key).isJsonNull()) {
return defaultValue;
}
return source.get(key).getAsLong();
}
private Integer getAsInteger(JsonObject source, String key) {
if (source == null || !source.has(key) || source.get(key).isJsonNull()) {
return null;
}
return source.get(key).getAsInt();
}
private int getAsInteger(JsonObject source, String key, int defaultValue) {
if (source == null || !source.has(key) || source.get(key).isJsonNull()) {
return defaultValue;
}
return source.get(key).getAsInt();
}
private boolean getAsBoolean(JsonObject source, String key) {
return source != null && source.has(key) && !source.get(key).isJsonNull() && source.get(key).getAsBoolean();
}
private String trimContainerName(String name) {
if (name == null) {
return null;
}
return name.startsWith("/") ? name.substring(1) : name;
}
}

View File

@@ -1,235 +1,72 @@
package com.imyeyu.api.modules.system.task;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.utils.OS;
import com.imyeyu.utils.Time;
import com.imyeyu.java.TimiJava;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import com.imyeyu.api.modules.system.task.status.StatusCollector;
import com.imyeyu.utils.Time;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
import oshi.hardware.GlobalMemory;
import oshi.hardware.HardwareAbstractionLayer;
import oshi.hardware.NetworkIF;
import oshi.software.os.OSFileStore;
import oshi.software.os.OperatingSystem;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.util.Deque;
import java.util.List;
/**
* 服务状态集任务
* 服务状态集任务
*
* @author 夜雨
* @version 2022-01-31 15:18
* @since 2022-01-31 15:18
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ServerStatusTask implements SchedulingConfigurer, TimiJava {
public class ServerStatusTask implements SchedulingConfigurer {
private final ServerStatus status;
private final SettingService settingService;
private GlobalMemory globalMemory; // 内存
private MemoryMXBean jvmMemory; // JVM 内存
private OperatingSystem os; // 操作系统
private CentralProcessor processor; // 中央处理器
private HardwareAbstractionLayer hardware; // 硬件
private long[] lastCPUTicks; // CPU 上一时刻状态
private long lastLinuxAPIMemoryUsed; // 上一周期 JVM 内存大小
private final List<StatusCollector> statusCollectors;
@Override
public void configureTasks(@NotNull ScheduledTaskRegistrar taskRegistrar) {
lastLinuxAPIMemoryUsed = -1;
SystemInfo systemInfo = new SystemInfo();
HardwareAbstractionLayer hardware = systemInfo.getHardware();
// 系统信息
SystemInfo system = new SystemInfo();
// 硬件信息
hardware = system.getHardware();
// 操作系统
os = system.getOperatingSystem();
// ---------- 静态数据 ----------
// 系统
status.getOs().setName(OS.NAME);
status.getOs().setBootAt(os.getSystemBootTime() * 1000);
// JVM
jvmMemory = ManagementFactory.getMemoryMXBean();
status.getJvm().setBootAt(Time.now());
status.getJvm().setName(System.getProperty("java.vm.name"));
status.getJvm().setVersion(System.getProperty("java.version"));
// CPU
processor = hardware.getProcessor();
status.getCpu().setName(processor.getProcessorIdentifier().getName().trim());
status.getCpu().setCoreCount(processor.getPhysicalProcessorCount());
status.getCpu().setLogicalCount(processor.getLogicalProcessorCount());
// 内存
globalMemory = hardware.getMemory();
status.getMemory().setSize(globalMemory.getTotal());
status.getMemory().setSwapSize(globalMemory.getVirtualMemory().getSwapTotal());
// 网卡
List<NetworkIF> networkIFs = hardware.getNetworkIFs();
{
NetworkIF networkIF;
boolean isFound = false;
for (int i = 0; i < networkIFs.size(); i++) {
networkIF = networkIFs.get(i);
if (networkIF.getMacaddr().equals(settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC))) {
status.getNetwork().setMac(networkIF.getMacaddr());
status.getNetwork().setRecvTotal(networkIF.getBytesRecv());
status.getNetwork().setSentTotal(networkIF.getBytesSent());
isFound = true;
break;
}
}
if (!isFound) {
log.error("not found setting networkIF MAC: %s" + settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC));
for (int i = 0; i < networkIFs.size(); i++) {
log.info("Network Interface: {} -> {}", networkIFs.get(i).getMacaddr(), networkIFs.get(i).getDisplayName());
}
StatusCollectContext context = new StatusCollectContext(
status,
hardware.getMemory(),
ManagementFactory.getMemoryMXBean(),
settingService,
hardware.getComputerSystem(),
systemInfo.getOperatingSystem(),
hardware.getProcessor(),
hardware,
Time.now()
);
synchronized (status) {
for (StatusCollector collector : statusCollectors) {
collector.initialize(context);
}
}
taskRegistrar.addTriggerTask(() -> {
long now = Time.now();
// ---------- JVM 内存 ----------
long nowLinuxAPIMemoryUsed = jvmMemory.getHeapMemoryUsage().getUsed();
long linuxAPIMemoryDiff = Math.abs(nowLinuxAPIMemoryUsed - lastLinuxAPIMemoryUsed);
lastLinuxAPIMemoryUsed = nowLinuxAPIMemoryUsed;
status.getJvm().getMemory().setInit(jvmMemory.getHeapMemoryUsage().getInit());
status.getJvm().getMemory().setMax(jvmMemory.getHeapMemoryUsage().getMax());
putDeque(status.getJvm().getMemory().getUsed(), nowLinuxAPIMemoryUsed);
putDeque(status.getJvm().getMemory().getCommitted(), jvmMemory.getHeapMemoryUsage().getCommitted());
// ---------- JVM GC ----------
long gcSyncCycles = 0;
long gcSyncCyclesTime = 0;
long gcPauses = 0;
long gcPausesTime = 0;
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
gcSyncCycles += gc.getCollectionCount();
gcSyncCyclesTime += gc.getCollectionTime();
switch (gc.getName()) {
case "ZGC Cycles" -> {
gcSyncCycles += gc.getCollectionCount();
gcSyncCyclesTime += gc.getCollectionTime();
synchronized (status) {
context.setCollectAt(Time.now());
for (StatusCollector collector : statusCollectors) {
collector.collect(context);
}
case "ZGC Pauses" -> {
gcPauses += gc.getCollectionCount();
gcPausesTime += gc.getCollectionTime();
if (status.getJvm().getZgc().getPauses() < gcPauses) {
// 发生 GC 回收
status.getJvm().getZgc().setLastPauseAt(now);
status.getJvm().getZgc().setLastRecoverySize(linuxAPIMemoryDiff);
status.getUpdateAxis().addLast(context.getCollectAt());
if (settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) < status.getUpdateAxis().size()) {
status.getUpdateAxis().pollFirst();
}
}
}
}
putDeque(status.getJvm().getZgc().getSyncCyclesTime(), gcSyncCyclesTime - status.getJvm().getZgc().getSyncCyclesTimeTotal());
putDeque(status.getJvm().getZgc().getPausesTime(), gcPausesTime - status.getJvm().getZgc().getPausesTimeTotal());
status.getJvm().getZgc().setSyncCycles(gcSyncCycles);
status.getJvm().getZgc().setSyncCyclesTimeTotal(gcSyncCyclesTime);
status.getJvm().getZgc().setPauses(gcPauses);
status.getJvm().getZgc().setPausesTimeTotal(gcPausesTime);
// ---------- CPU ----------
if (lastCPUTicks != null) {
long[] ticks = processor.getSystemCpuLoadTicks();
long user = ticks[CentralProcessor.TickType.USER.getIndex()] - lastCPUTicks[CentralProcessor.TickType.USER.getIndex()];
long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - lastCPUTicks[CentralProcessor.TickType.NICE.getIndex()];
long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - lastCPUTicks[CentralProcessor.TickType.SYSTEM.getIndex()];
long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IDLE.getIndex()];
long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IOWAIT.getIndex()];
long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IRQ.getIndex()];
long softIRQ = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - lastCPUTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()];
long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - lastCPUTicks[CentralProcessor.TickType.STEAL.getIndex()];
long total = user + nice + sys + idle + ioWait + irq + softIRQ + steal;
putDeque(status.getCpu().getSystem(), 100D * sys / total);
putDeque(status.getCpu().getUsed(), 100 - 100D * idle / total);
}
lastCPUTicks = processor.getSystemCpuLoadTicks();
status.getCpu().setTemperature(hardware.getSensors().getCpuTemperature());
// ---------- 内存 ----------
putDeque(status.getMemory().getUsed(), globalMemory.getTotal() - globalMemory.getAvailable());
putDeque(status.getMemory().getSwapUsed(), globalMemory.getVirtualMemory().getSwapUsed());
// ---------- 网络 ----------
networkIFs.clear();
networkIFs.addAll(hardware.getNetworkIFs());
NetworkIF networkIF;
for (int i = 0; i < networkIFs.size(); i++) {
networkIF = networkIFs.get(i);
if (networkIF.getMacaddr().equals(settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC))) {
long recv = networkIF.getBytesRecv() - status.getNetwork().getRecvTotal();
long sent = networkIF.getBytesSent() - status.getNetwork().getSentTotal();
status.getNetwork().setRecvNow(recv / (settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000));
status.getNetwork().setSentNow(sent / (settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000));
status.getNetwork().setRecvTotal(networkIF.getBytesRecv());
status.getNetwork().setSentTotal(networkIF.getBytesSent());
putDeque(status.getNetwork().getRecv(), recv);
putDeque(status.getNetwork().getSent(), sent);
break;
}
}
// ---------- 磁盘分区 ----------
ServerStatus.Partition partition;
status.getPartitions().clear();
// 分区从文件系统获取,而非物理分区
List<OSFileStore> fileStores = os.getFileSystem().getFileStores();
for (OSFileStore fileStore : fileStores) {
partition = new ServerStatus.Partition();
partition.setUuid(fileStore.getUUID());
partition.setPath(fileStore.getMount());
partition.setType(fileStore.getType());
partition.setUsed(fileStore.getTotalSpace() - fileStore.getUsableSpace());
partition.setTotal(fileStore.getTotalSpace());
status.getPartitions().add(partition);
}
// ---------- 更新时轴 ----------
putDeque(status.getUpdateAxis(), Time.now());
}, tc -> new CronTrigger("0/%s * * * * ?".formatted(settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)).nextExecution(tc));
}
/**
* 有所限制地添加队列数据,达到配置 {@link SettingKey#SYSTEM_STATUS_LIMIT} 个时移除最旧的
*
* @param deque 队列
* @param t 数据
* @param <T> 数据类型
*/
private <T> void putDeque(Deque<T> deque, T t) {
deque.addLast(t);
if (settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) < deque.size()) {
deque.pollFirst();
}
}, triggerContext -> new CronTrigger("0/%s * * * * ?".formatted(settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)).nextExecution(triggerContext));
}
}

View File

@@ -0,0 +1,89 @@
package com.imyeyu.api.modules.system.task;
import com.imyeyu.api.modules.system.bean.UpsStatusStore;
import com.imyeyu.api.modules.system.util.UpsStatusClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* UPS 状态采集任务
*
* @author Codex
* @since 2026-04-07
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UpsStatusTask implements SchedulingConfigurer {
public static final long MAX_HISTORY_MS = 3L * 24 * 60 * 60 * 1000;
private final UpsStatusClient upsStatusClient;
private final UpsStatusStore upsStatusStore;
@Value("${ups.collect-enabled:true}")
private boolean collectEnabled;
@Value("${ups.collect-rate-ms:60000}")
private long collectRateMs;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
if (!collectEnabled) {
return;
}
PeriodicTrigger trigger = new PeriodicTrigger(collectRateMs, TimeUnit.MILLISECONDS);
trigger.setInitialDelay(0);
taskRegistrar.addTriggerTask(this::collect, trigger);
}
/**
* 立即采集一次
*/
public void collectOnce() {
collect();
}
private void collect() {
try {
UpsStatusStore.Snapshot snapshot = upsStatusClient.fetchSnapshot();
long now = System.currentTimeMillis();
snapshot.setUpdatedAt(now);
UpsStatusStore.Point point = new UpsStatusStore.Point();
point.setAt(now);
point.setUpsTime(snapshot.getUpsTime());
point.setWorkMode(snapshot.getWorkMode());
point.setInputVoltage(snapshot.getInputVoltage());
point.setInputFrequency(snapshot.getInputFrequency());
point.setOutputVoltage(snapshot.getOutputVoltage());
point.setOutputFrequency(snapshot.getOutputFrequency());
point.setOutputLoadPercent(snapshot.getOutputLoadPercent());
point.setBatteryVoltage(snapshot.getBatteryVoltage());
point.setBatteryCapacity(snapshot.getBatteryCapacity());
point.setBatteryRemainTime(snapshot.getBatteryRemainTime());
point.setTemperature(snapshot.getTemperature());
point.setBypassActive(snapshot.isBypassActive());
point.setShutdownActive(snapshot.isShutdownActive());
point.setOutputOn(snapshot.isOutputOn());
synchronized (upsStatusStore) {
upsStatusStore.setCurrent(snapshot);
upsStatusStore.getHistory().addLast(point);
long threshold = now - MAX_HISTORY_MS;
while (!upsStatusStore.getHistory().isEmpty() && upsStatusStore.getHistory().peekFirst().getAt() < threshold) {
upsStatusStore.getHistory().pollFirst();
}
}
} catch (Exception e) {
log.error("collect ups status error", e);
}
}
}

View File

@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.system.task.status;
import com.imyeyu.api.modules.common.bean.SettingKey;
import java.util.Deque;
/**
* 带有限长队列工具的采集器基类
*
* @author Codex
* @since 2026-04-06
*/
public abstract class AbstractDequeStatusCollector implements StatusCollector {
/**
* 有限长度追加队列数据
*
* @param context 采集上下文
* @param deque 队列
* @param value 值
* @param <T> 类型
*/
protected <T> void putDeque(StatusCollectContext context, Deque<T> deque, T value) {
deque.addLast(value);
if (context.getSettingService().getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) < deque.size()) {
deque.pollFirst();
}
}
}

View File

@@ -0,0 +1,37 @@
package com.imyeyu.api.modules.system.task.status;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import oshi.hardware.CentralProcessor;
import oshi.hardware.ComputerSystem;
import oshi.hardware.GlobalMemory;
import oshi.hardware.HardwareAbstractionLayer;
import oshi.software.os.OperatingSystem;
import java.lang.management.MemoryMXBean;
/**
* 状态采集上下文
*
* @author Codex
* @since 2026-04-06
*/
@Getter
@AllArgsConstructor
public class StatusCollectContext {
private final ServerStatus status;
private final GlobalMemory globalMemory;
private final MemoryMXBean jvmMemory;
private final SettingService settingService;
private final ComputerSystem computerSystem;
private final OperatingSystem operatingSystem;
private final CentralProcessor processor;
private final HardwareAbstractionLayer hardware;
@Setter
private long collectAt;
}

View File

@@ -0,0 +1,25 @@
package com.imyeyu.api.modules.system.task.status;
/**
* 状态采集器
*
* @author Codex
* @since 2026-04-06
*/
public interface StatusCollector {
/**
* 初始化采集器
*
* @param context 采集上下文
*/
default void initialize(StatusCollectContext context) {
}
/**
* 采集状态
*
* @param context 采集上下文
*/
void collect(StatusCollectContext context);
}

View File

@@ -0,0 +1,50 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import oshi.hardware.CentralProcessor;
/**
* CPU 状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:18
*/
@Component
@Order(20)
public class CpuStatusCollector extends AbstractDequeStatusCollector {
private long[] lastCpuTicks;
@Override
public void initialize(StatusCollectContext context) {
context.getStatus().getCpu().setName(context.getProcessor().getProcessorIdentifier().getName().trim());
context.getStatus().getCpu().setCoreCount(context.getProcessor().getPhysicalProcessorCount());
context.getStatus().getCpu().setLogicalCount(context.getProcessor().getLogicalProcessorCount());
lastCpuTicks = context.getProcessor().getSystemCpuLoadTicks();
}
@Override
public void collect(StatusCollectContext context) {
long[] ticks = context.getProcessor().getSystemCpuLoadTicks();
if (lastCpuTicks != null) {
long user = ticks[CentralProcessor.TickType.USER.getIndex()] - lastCpuTicks[CentralProcessor.TickType.USER.getIndex()];
long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - lastCpuTicks[CentralProcessor.TickType.NICE.getIndex()];
long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - lastCpuTicks[CentralProcessor.TickType.SYSTEM.getIndex()];
long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IDLE.getIndex()];
long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IOWAIT.getIndex()];
long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IRQ.getIndex()];
long softIrq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - lastCpuTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()];
long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - lastCpuTicks[CentralProcessor.TickType.STEAL.getIndex()];
long total = user + nice + sys + idle + ioWait + irq + softIrq + steal;
if (0 < total) {
putDeque(context, context.getStatus().getCpu().getSystem(), 100D * sys / total);
putDeque(context, context.getStatus().getCpu().getUsed(), 100 - 100D * idle / total);
}
}
lastCpuTicks = ticks;
context.getStatus().getCpu().setTemperature(context.getHardware().getSensors().getCpuTemperature());
}
}

View File

@@ -0,0 +1,46 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import com.imyeyu.api.modules.system.task.status.StatusCollector;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* 硬件状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:18
*/
@Component
@Order(35)
public class HardwareStatusCollector implements StatusCollector {
@Override
public void initialize(StatusCollectContext context) {
ServerStatus.Baseboard baseboard = context.getStatus().getHardware().getBaseboard();
baseboard.setManufacturer(context.getComputerSystem().getBaseboard().getManufacturer());
baseboard.setModel(context.getComputerSystem().getBaseboard().getModel());
baseboard.setVersion(context.getComputerSystem().getBaseboard().getVersion());
baseboard.setSerialNumber(context.getComputerSystem().getBaseboard().getSerialNumber());
ServerStatus.Firmware firmware = context.getStatus().getHardware().getFirmware();
firmware.setManufacturer(context.getComputerSystem().getFirmware().getManufacturer());
firmware.setName(context.getComputerSystem().getFirmware().getName());
firmware.setDescription(context.getComputerSystem().getFirmware().getDescription());
firmware.setVersion(context.getComputerSystem().getFirmware().getVersion());
firmware.setReleaseDate(context.getComputerSystem().getFirmware().getReleaseDate());
}
@Override
public void collect(StatusCollectContext context) {
int[] fanSpeeds = context.getHardware().getSensors().getFanSpeeds();
ArrayList<Integer> values = new ArrayList<>(fanSpeeds.length);
for (int fanSpeed : fanSpeeds) {
values.add(fanSpeed);
}
context.getStatus().getHardware().setFanSpeeds(values);
}
}

View File

@@ -0,0 +1,100 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import com.imyeyu.utils.Time;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;
/**
* JVM 状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:17
*/
@Component
@Order(40)
public class JvmStatusCollector extends AbstractDequeStatusCollector {
private long lastHeapUsed = -1;
@Override
public void initialize(StatusCollectContext context) {
context.getStatus().getJvm().setBootAt(Time.now());
context.getStatus().getJvm().setName(System.getProperty("java.vm.name"));
context.getStatus().getJvm().setVersion(System.getProperty("java.version"));
context.getStatus().getJvm().setGcName(resolveGcName());
}
@Override
public void collect(StatusCollectContext context) {
long heapUsed = context.getJvmMemory().getHeapMemoryUsage().getUsed();
context.getStatus().getJvm().getMemory().setInit(context.getJvmMemory().getHeapMemoryUsage().getInit());
context.getStatus().getJvm().getMemory().setMax(context.getJvmMemory().getHeapMemoryUsage().getMax());
putDeque(context, context.getStatus().getJvm().getMemory().getUsed(), heapUsed);
putDeque(context, context.getStatus().getJvm().getMemory().getCommitted(), context.getJvmMemory().getHeapMemoryUsage().getCommitted());
long recoverySize = 0;
if (0 <= lastHeapUsed) {
recoverySize = Math.abs(heapUsed - lastHeapUsed);
}
lastHeapUsed = heapUsed;
long gcSyncCycles = 0;
long gcSyncCyclesTime = 0;
long gcPauses = 0;
long gcPausesTime = 0;
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
switch (gc.getName()) {
case "ZGC Cycles" -> {
gcSyncCycles += gc.getCollectionCount();
gcSyncCyclesTime += gc.getCollectionTime();
}
case "ZGC Pauses" -> {
gcPauses += gc.getCollectionCount();
gcPausesTime += gc.getCollectionTime();
}
default -> {
}
}
}
if (context.getStatus().getJvm().getGc().getPauses() < gcPauses) {
context.getStatus().getJvm().getGc().setLastPauseAt(context.getCollectAt());
context.getStatus().getJvm().getGc().setLastRecoverySize(recoverySize);
}
putDeque(context, context.getStatus().getJvm().getGc().getSyncCyclesTime(), gcSyncCyclesTime - context.getStatus().getJvm().getGc().getSyncCyclesTimeTotal());
putDeque(context, context.getStatus().getJvm().getGc().getPausesTime(), gcPausesTime - context.getStatus().getJvm().getGc().getPausesTimeTotal());
context.getStatus().getJvm().getGc().setSyncCycles(gcSyncCycles);
context.getStatus().getJvm().getGc().setSyncCyclesTimeTotal(gcSyncCyclesTime);
context.getStatus().getJvm().getGc().setPauses(gcPauses);
context.getStatus().getJvm().getGc().setPausesTimeTotal(gcPausesTime);
}
/**
* 解析当前 JVM 的主要 GC 名称
*
* @return GC 名称
*/
private String resolveGcName() {
List<GarbageCollectorMXBean> collectors = ManagementFactory.getGarbageCollectorMXBeans();
if (collectors.isEmpty()) {
return null;
}
if (1 == collectors.size()) {
return collectors.get(0).getName();
}
StringBuilder gcName = new StringBuilder();
for (GarbageCollectorMXBean collector : collectors) {
if (!gcName.isEmpty()) {
gcName.append(", ");
}
gcName.append(collector.getName());
}
return gcName.toString();
}
}

View File

@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 系统内存状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:17
*/
@Component
@Order(30)
public class MemoryStatusCollector extends AbstractDequeStatusCollector {
@Override
public void initialize(StatusCollectContext context) {
context.getStatus().getMemory().setSize(context.getGlobalMemory().getTotal());
context.getStatus().getMemory().setSwapSize(context.getGlobalMemory().getVirtualMemory().getSwapTotal());
}
@Override
public void collect(StatusCollectContext context) {
putDeque(context, context.getStatus().getMemory().getUsed(), context.getGlobalMemory().getTotal() - context.getGlobalMemory().getAvailable());
putDeque(context, context.getStatus().getMemory().getSwapUsed(), context.getGlobalMemory().getVirtualMemory().getSwapUsed());
}
}

View File

@@ -0,0 +1,80 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.librealsense.context;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import oshi.hardware.NetworkIF;
import java.util.List;
/**
* 网络状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:15
*/
@Slf4j
@Component
@Order(50)
public class NetworkStatusCollector extends AbstractDequeStatusCollector {
@Override
public void initialize(StatusCollectContext context) {
List<NetworkIF> networkIFs = context.getHardware().getNetworkIFs();
String targetMac = context.getSettingService().getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC);
for (NetworkIF networkIF : networkIFs) {
if (networkIF.getMacaddr().equals(targetMac)) {
networkIF.updateAttributes();
updateNetworkStatus(context, networkIF);
return;
}
}
log.error("not found setting networkIF MAC: {}", targetMac);
for (NetworkIF networkIF : networkIFs) {
log.info("Network Interface: {} -> {}", networkIF.getMacaddr(), networkIF.getDisplayName());
}
}
@Override
public void collect(StatusCollectContext context) {
String targetMac = context.getSettingService().getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC);
int sampleRateMs = context.getSettingService().getAsInt(SettingKey.SYSTEM_STATUS_RATE);
for (NetworkIF networkIF : context.getHardware().getNetworkIFs()) {
if (networkIF.getMacaddr().equals(targetMac)) {
networkIF.updateAttributes();
long recv = networkIF.getBytesRecv() - context.getStatus().getNetwork().getRecvTotal();
long sent = networkIF.getBytesSent() - context.getStatus().getNetwork().getSentTotal();
context.getStatus().getNetwork().setRecvNow(recv * 1000 / sampleRateMs);
context.getStatus().getNetwork().setSentNow(sent * 1000 / sampleRateMs);
updateNetworkStatus(context, networkIF);
putDeque(context, context.getStatus().getNetwork().getRecv(), recv);
putDeque(context, context.getStatus().getNetwork().getSent(), sent);
return;
}
}
}
/**
* 更新网络状态缓存
*
* @param context 采集上下文
* @param networkIF 网卡
*/
private void updateNetworkStatus(StatusCollectContext context, NetworkIF networkIF) {
context.getStatus().getNetwork().setName(networkIF.getDisplayName());
context.getStatus().getNetwork().setMac(networkIF.getMacaddr());
context.getStatus().getNetwork().setRecvTotal(networkIF.getBytesRecv());
context.getStatus().getNetwork().setSentTotal(networkIF.getBytesSent());
context.getStatus().getNetwork().setRecvPacketsTotal(networkIF.getPacketsRecv());
context.getStatus().getNetwork().setSentPacketsTotal(networkIF.getPacketsSent());
context.getStatus().getNetwork().setInErrors(networkIF.getInErrors());
context.getStatus().getNetwork().setOutErrors(networkIF.getOutErrors());
context.getStatus().getNetwork().setInDrops(networkIF.getInDrops());
context.getStatus().getNetwork().setCollisions(networkIF.getCollisions());
}
}

View File

@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import com.imyeyu.api.modules.system.task.status.StatusCollector;
import com.imyeyu.utils.OS;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 操作系统状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:15
*/
@Component
@Order(10)
public class OSStatusCollector implements StatusCollector {
@Override
public void initialize(StatusCollectContext context) {
context.getStatus().getOs().setName(OS.NAME);
context.getStatus().getOs().setBootAt(context.getOperatingSystem().getSystemBootTime() * 1000);
}
@Override
public void collect(StatusCollectContext context) {
}
}

View File

@@ -0,0 +1,90 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import com.imyeyu.api.modules.system.task.status.StatusCollector;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import oshi.hardware.HWDiskStore;
import oshi.hardware.HWPartition;
import oshi.software.os.OSFileStore;
import java.util.HashMap;
import java.util.Map;
/**
* 存储状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:14
*/
@Component
@Order(60)
public class StorageStatusCollector implements StatusCollector {
@Override
public void collect(StatusCollectContext context) {
Map<String, OSFileStore> fileStoreMap = createFileStoreMap(context);
context.getStatus().getStoragePartitions().clear();
for (HWDiskStore diskStore : context.getHardware().getDiskStores()) {
diskStore.updateAttributes();
for (HWPartition partition : diskStore.getPartitions()) {
ServerStatus.StoragePartition item = new ServerStatus.StoragePartition();
item.setDiskName(diskStore.getName());
item.setDiskModel(diskStore.getModel());
item.setDiskSerial(diskStore.getSerial());
item.setPartitionId(partition.getIdentification());
item.setPartitionName(partition.getName());
item.setPartitionType(partition.getType());
item.setUuid(partition.getUuid());
item.setMountPoint(partition.getMountPoint());
item.setTotalBytes(partition.getSize());
item.setTransferTimeMs(diskStore.getTransferTime());
OSFileStore fileStore = matchFileStore(partition, fileStoreMap);
if (fileStore != null) {
fileStore.updateAttributes();
item.setUsedBytes(fileStore.getTotalSpace() - fileStore.getUsableSpace());
}
context.getStatus().getStoragePartitions().add(item);
}
}
}
/**
* 创建文件系统映射
*
* @param context 采集上下文
* @return 映射表
*/
private Map<String, OSFileStore> createFileStoreMap(StatusCollectContext context) {
Map<String, OSFileStore> result = new HashMap<>();
for (OSFileStore fileStore : context.getOperatingSystem().getFileSystem().getFileStores()) {
result.put("mount:" + fileStore.getMount(), fileStore);
result.put("volume:" + fileStore.getVolume(), fileStore);
result.put("name:" + fileStore.getName(), fileStore);
}
return result;
}
/**
* 匹配文件系统
*
* @param partition 物理分区
* @param fileStoreMap 文件系统映射
* @return 文件系统
*/
private OSFileStore matchFileStore(HWPartition partition, Map<String, OSFileStore> fileStoreMap) {
if (partition.getMountPoint() != null && !partition.getMountPoint().isBlank()) {
OSFileStore byMount = fileStoreMap.get("mount:" + partition.getMountPoint());
if (byMount != null) {
return byMount;
}
}
OSFileStore byName = fileStoreMap.get("name:" + partition.getName());
if (byName != null) {
return byName;
}
return fileStoreMap.get("volume:" + partition.getIdentification());
}
}

View File

@@ -0,0 +1,274 @@
package com.imyeyu.api.modules.system.util;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.UnixDomainSocketAddress;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Docker Engine API 客户端
*
* @author Codex
* @since 2026-04-06
*/
@Slf4j
@Component
public class DockerEngineClient {
private final String host;
private final String apiVersion;
private final Duration timeout;
private final HttpClient httpClient = HttpClient.newBuilder().build();
public DockerEngineClient(
@Value("${docker.engine.host:unix:///var/run/docker.sock}") String host,
@Value("${docker.engine.api-version:v1.41}") String apiVersion,
@Value("${docker.engine.timeout-ms:5000}") long timeoutMs
) {
this.host = host;
this.apiVersion = apiVersion;
this.timeout = Duration.ofMillis(timeoutMs);
}
/**
* 获取 JSON 响应
*
* @param path 接口路径
* @param queryParams 查询参数
* @return JSON 数据
*/
public JsonElement getJson(String path, Map<String, String> queryParams) {
String requestPath = buildRequestPath(path, queryParams);
try {
String body = host.startsWith("unix://")
? executeUnixGet(requestPath)
: executeHttpGet(requestPath);
return JsonParser.parseString(body);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("docker engine request interrupted: {}", requestPath, e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO docker engine request interrupted");
} catch (IOException e) {
log.error("docker engine request error: {}", requestPath, e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO docker engine request error");
}
}
/**
* 构建请求路径
*
* @param path 接口路径
* @param queryParams 查询参数
* @return 请求路径
*/
private String buildRequestPath(String path, Map<String, String> queryParams) {
StringBuilder builder = new StringBuilder();
builder.append("/");
builder.append(apiVersion);
if (!path.startsWith("/")) {
builder.append("/");
}
builder.append(path);
if (queryParams != null && !queryParams.isEmpty()) {
builder.append("?");
boolean first = true;
for (Map.Entry<String, String> item : queryParams.entrySet()) {
if (!first) {
builder.append("&");
}
first = false;
builder.append(item.getKey()).append("=").append(item.getValue());
}
}
return builder.toString();
}
/**
* 通过 HTTP/TCP 执行 GET
*
* @param requestPath 请求路径
* @return 响应体
* @throws IOException IO 异常
* @throws InterruptedException 中断异常
*/
private String executeHttpGet(String requestPath) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(host + requestPath))
.timeout(timeout)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (400 <= response.statusCode()) {
throw new IOException("docker engine http error: " + response.statusCode());
}
return response.body();
}
/**
* 通过 Unix Socket 执行 GET
*
* @param requestPath 请求路径
* @return 响应体
* @throws IOException IO 异常
*/
private String executeUnixGet(String requestPath) throws IOException {
String socketPath = host.substring("unix://".length());
UnixDomainSocketAddress address = UnixDomainSocketAddress.of(Path.of(socketPath));
try (SocketChannel channel = SocketChannel.open(address)) {
channel.configureBlocking(true);
String requestText = """
GET %s HTTP/1.1\r
Host: docker\r
Accept: application/json\r
Connection: close\r
\r
""".formatted(requestPath);
channel.write(StandardCharsets.UTF_8.encode(requestText));
byte[] responseBytes = readAll(channel);
return parseHttpBody(responseBytes);
}
}
/**
* 读取全部响应字节
*
* @param channel 通道
* @return 字节数组
* @throws IOException IO 异常
*/
private byte[] readAll(SocketChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(8192);
ByteArrayOutputStream output = new ByteArrayOutputStream();
while (true) {
int readLength = channel.read(buffer);
if (readLength < 0) {
break;
}
if (0 == readLength) {
continue;
}
buffer.flip();
output.write(buffer.array(), 0, buffer.remaining());
buffer.clear();
}
return output.toByteArray();
}
/**
* 解析 HTTP 响应体
*
* @param responseBytes 响应字节
* @return 响应体
* @throws IOException IO 异常
*/
private String parseHttpBody(byte[] responseBytes) throws IOException {
int splitIndex = indexOf(responseBytes, new byte[] {'\r', '\n', '\r', '\n'});
if (splitIndex < 0) {
throw new IOException("invalid docker engine response");
}
String headerText = new String(responseBytes, 0, splitIndex, StandardCharsets.UTF_8);
byte[] bodyBytes = new byte[responseBytes.length - splitIndex - 4];
System.arraycopy(responseBytes, splitIndex + 4, bodyBytes, 0, bodyBytes.length);
if (headerText.contains("Transfer-Encoding: chunked")) {
bodyBytes = decodeChunkedBody(bodyBytes);
}
if (!headerText.startsWith("HTTP/1.1 200") && !headerText.startsWith("HTTP/1.1 204")) {
throw new IOException("docker engine http error: " + headerText.lines().findFirst().orElse(headerText));
}
return new String(bodyBytes, StandardCharsets.UTF_8);
}
/**
* 解码 Chunked 响应体
*
* @param bodyBytes 原始响应体
* @return 解码后的响应体
* @throws IOException IO 异常
*/
private byte[] decodeChunkedBody(byte[] bodyBytes) throws IOException {
int offset = 0;
ByteArrayOutputStream output = new ByteArrayOutputStream();
while (offset < bodyBytes.length) {
int lineEnd = indexOf(bodyBytes, offset, new byte[] {'\r', '\n'});
if (lineEnd < 0) {
break;
}
String sizeText = new String(bodyBytes, offset, lineEnd - offset, StandardCharsets.UTF_8).trim();
int chunkSize = Integer.parseInt(sizeText, 16);
offset = lineEnd + 2;
if (0 == chunkSize) {
break;
}
output.write(bodyBytes, offset, chunkSize);
offset += chunkSize + 2;
}
return output.toByteArray();
}
/**
* 查找字节序列
*
* @param source 原数组
* @param pattern 目标数组
* @return 下标
*/
private int indexOf(byte[] source, byte[] pattern) {
return indexOf(source, 0, pattern);
}
/**
* 从指定位置查找字节序列
*
* @param source 原数组
* @param start 起始位置
* @param pattern 目标数组
* @return 下标
*/
private int indexOf(byte[] source, int start, byte[] pattern) {
for (int i = start; i <= source.length - pattern.length; i++) {
boolean matched = true;
for (int j = 0; j < pattern.length; j++) {
if (source[i + j] != pattern[j]) {
matched = false;
break;
}
}
if (matched) {
return i;
}
}
return -1;
}
/**
* 创建查询参数映射
*
* @param entries 键值对
* @return 映射
*/
public static Map<String, String> query(String... entries) {
LinkedHashMap<String, String> result = new LinkedHashMap<>();
for (int i = 0; i + 1 < entries.length; i += 2) {
result.put(entries[i], entries[i + 1]);
}
return result;
}
}

View File

@@ -29,9 +29,9 @@ public class SystemAPIInterceptor implements HandlerInterceptor {
private final SettingService settingService;
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
String key = TimiSpring.getHeader("Key");
String key = TimiSpring.getHeader("Token");
if (TimiJava.isEmpty(key)) {
key = req.getParameter("key");
key = req.getParameter("token");
}
String dbKey = settingService.getAsString(SettingKey.SYSTEM_API_KEY);
String dbSuperKey = settingService.getAsString(SettingKey.SYSTEM_API_SUPER_KEY);

View File

@@ -0,0 +1,265 @@
package com.imyeyu.api.modules.system.util;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.imyeyu.api.modules.system.bean.UpsStatusStore;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
/**
* UPS 状态客户端
*
* @author Codex
* @since 2026-04-07
*/
@Slf4j
@Component
public class UpsStatusClient {
private final String statusUrl;
private final Duration timeout;
private final HttpClient httpClient = HttpClient.newBuilder().build();
public UpsStatusClient(
@Value("${ups.status-url:}") String statusUrl,
@Value("${ups.timeout-ms:5000}") long timeoutMs
) {
this.statusUrl = statusUrl;
this.timeout = Duration.ofMillis(timeoutMs);
}
/**
* 获取 UPS 状态 JSON
*
* @return JSON 数据
*/
public JsonElement getStatusJson() {
if (statusUrl == null || statusUrl.isBlank()) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少配置ups.status-url");
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(statusUrl))
.timeout(timeout)
.GET()
.build();
try {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (400 <= response.statusCode()) {
throw new IOException("ups http error: " + response.statusCode());
}
return JsonParser.parseString(response.body());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("request ups status interrupted: {}", statusUrl, e);
throw new TimiException(TimiCode.ERROR).msgKey("UPS 状态请求被中断");
} catch (IOException e) {
log.error("request ups status error: {}", statusUrl, e);
throw new TimiException(TimiCode.ERROR).msgKey("UPS 状态请求失败");
}
}
/**
* 拉取并解析 UPS 当前快照
*
* @return UPS 当前快照
*/
public UpsStatusStore.Snapshot fetchSnapshot() {
JsonObject root = getStatusJson().getAsJsonObject();
JsonObject workInfo = getAsObject(root, "workInfo");
UpsStatusStore.Snapshot snapshot = new UpsStatusStore.Snapshot();
snapshot.setUpsTime(readUpsTime(workInfo));
snapshot.setHostName(readMeaningfulText(root, "hostName"));
snapshot.setCustomer(readMeaningfulText(root, "customer"));
snapshot.setVersion(readMeaningfulText(root, "version"));
snapshot.setDeviceId(readMeaningfulText(workInfo, "deviceId"));
snapshot.setUpsType(readMeaningfulText(workInfo, "upsType"));
snapshot.setMorphological(readMeaningfulText(workInfo, "morphological"));
snapshot.setIoPhase(readMeaningfulText(workInfo, "ioPhase"));
snapshot.setWorkMode(readMeaningfulText(workInfo, "workMode"));
snapshot.setInputVoltage(readMeaningfulDouble(workInfo, "inputVoltage"));
snapshot.setInputFrequency(readMeaningfulDouble(workInfo, "inputFrequency"));
snapshot.setOutputVoltage(readMeaningfulDouble(workInfo, "outputVoltage"));
snapshot.setOutputFrequency(readMeaningfulDouble(workInfo, "outputFrequency"));
snapshot.setOutputLoadPercent(readMeaningfulInteger(workInfo, "outputLoadPercent"));
snapshot.setBatteryVoltage(readMeaningfulDouble(workInfo, "batteryVoltage"));
snapshot.setBatteryCapacity(readMeaningfulInteger(workInfo, "batteryCapacity"));
snapshot.setBatteryRemainTime(readMeaningfulInteger(workInfo, "batteryRemainTime"));
snapshot.setTemperature(readMeaningfulDouble(workInfo, "temperatureView"));
snapshot.setBypassActive(readAsBoolean(workInfo, "bypassActive"));
snapshot.setShutdownActive(readAsBoolean(workInfo, "shutdownActive"));
snapshot.setOutputOn(readAsBoolean(workInfo, "outputON"));
snapshot.setCharging(readAsBoolean(workInfo, "chargeON"));
snapshot.setFaultType(readMeaningfulText(workInfo, "faultType"));
snapshot.setFaultKind(readMeaningfulText(workInfo, "faultKind"));
snapshot.setWarnings(readWarnings(workInfo));
return snapshot;
}
/**
* 读取 UPS 时间戳
*
* @param workInfo 工作信息
* @return 时间戳
*/
private Long readUpsTime(JsonObject workInfo) {
JsonObject currentTime = getAsObject(workInfo, "currentTime");
if (currentTime == null || !currentTime.has("time") || currentTime.get("time").isJsonNull()) {
return null;
}
return currentTime.get("time").getAsLong();
}
/**
* 读取告警列表
*
* @param workInfo 工作信息
* @return 告警列表
*/
private java.util.List<String> readWarnings(JsonObject workInfo) {
java.util.List<String> warnings = new java.util.ArrayList<>();
if (workInfo == null || !workInfo.has("warnings") || !workInfo.get("warnings").isJsonArray()) {
return warnings;
}
JsonArray array = workInfo.getAsJsonArray("warnings");
for (JsonElement item : array) {
if (!item.isJsonPrimitive()) {
continue;
}
String warning = normalizeText(item.getAsString());
if (warning != null) {
warnings.add(warning);
}
}
return warnings;
}
/**
* 读取对象字段
*
* @param source 源对象
* @param key 键
* @return 子对象
*/
private JsonObject getAsObject(JsonObject source, String key) {
if (source == null || !source.has(key) || source.get(key).isJsonNull() || !source.get(key).isJsonObject()) {
return null;
}
return source.getAsJsonObject(key);
}
/**
* 读取有效文本
*
* @param source 源对象
* @param key 键
* @return 文本
*/
private String readMeaningfulText(JsonObject source, String key) {
if (source == null || !source.has(key) || source.get(key).isJsonNull()) {
return null;
}
return normalizeText(source.get(key).getAsString());
}
/**
* 读取有效整数
*
* @param source 源对象
* @param key 键
* @return 整数
*/
private Integer readMeaningfulInteger(JsonObject source, String key) {
if (source == null || !source.has(key) || source.get(key).isJsonNull()) {
return null;
}
JsonElement element = source.get(key);
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) {
return element.getAsInt();
}
String text = normalizeText(element.getAsString());
if (text == null) {
return null;
}
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
return null;
}
}
/**
* 读取有效小数
*
* @param source 源对象
* @param key 键
* @return 小数
*/
private Double readMeaningfulDouble(JsonObject source, String key) {
if (source == null || !source.has(key) || source.get(key).isJsonNull()) {
return null;
}
JsonElement element = source.get(key);
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) {
return element.getAsDouble();
}
String text = normalizeText(element.getAsString());
if (text == null) {
return null;
}
try {
return Double.parseDouble(text);
} catch (NumberFormatException e) {
return null;
}
}
/**
* 读取布尔值
*
* @param source 源对象
* @param key 键
* @return 布尔值
*/
private boolean readAsBoolean(JsonObject source, String key) {
return source != null && source.has(key) && !source.get(key).isJsonNull() && source.get(key).getAsBoolean();
}
/**
* 规范化文本
*
* @param text 原始文本
* @return 规范化后的文本
*/
private String normalizeText(String text) {
if (text == null) {
return null;
}
String normalized = text.trim();
if (normalized.isEmpty()) {
return null;
}
if ("----".equals(normalized)) {
return null;
}
if (normalized.startsWith("----:")) {
return null;
}
return normalized;
}
}

View File

@@ -0,0 +1,381 @@
package com.imyeyu.api.modules.system.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 系统状态数据视图
*
* @author Codex
* @since 2026-04-06
*/
public class SystemStatusDataView {
/**
* 当前快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Snapshot {
/** 操作系统 */
private OS os;
/** CPU */
private CPU cpu;
/** 系统内存 */
private Memory memory;
/** JVM */
private JVM jvm;
/** 网络 */
private Network network;
/** 硬件 */
private Hardware hardware;
/** 存储分区 */
private List<StoragePartition> storagePartitions = new ArrayList<>();
}
/**
* 历史点
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Point {
/** 采样时间 */
private long at;
/** CPU 总占用 */
private Double cpuUsagePercent;
/** CPU 系统占用 */
private Double cpuSystemPercent;
/** 系统已用内存 */
private Long memoryUsedBytes;
/** 已用交换分区 */
private Long swapUsedBytes;
/** JVM 已用堆内存 */
private Long heapUsedBytes;
/** JVM 已提交堆内存 */
private Long heapCommittedBytes;
/** GC 周期耗时 */
private Long gcCycleTimeMs;
/** GC 暂停耗时 */
private Long gcPauseTimeMs;
/** 接收速率 */
private Long rxBytesPerSecond;
/** 发送速率 */
private Long txBytesPerSecond;
}
/**
* 操作系统快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class OS {
/** 系统名称 */
private String name;
/** 启动时间 */
private long bootAt;
/** 运行时长 */
private long uptimeMs;
}
/**
* CPU 快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class CPU {
/** 型号 */
private String model;
/** 物理核心数 */
private int physicalCores;
/** 逻辑核心数 */
private int logicalCores;
/** 总占用 */
private Double usagePercent;
/** 系统占用 */
private Double systemPercent;
/** 温度 */
private double temperatureCelsius;
}
/**
* 内存快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Memory {
/** 总内存 */
private long totalBytes;
/** 已用内存 */
private Long usedBytes;
/** 使用率 */
private Double usagePercent;
/** 交换分区总量 */
private long swapTotalBytes;
/** 已用交换分区 */
private Long swapUsedBytes;
}
/**
* JVM 快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class JVM {
/** 名称 */
private String name;
/** 版本 */
private String version;
/** 启动时间 */
private long bootAt;
/** 初始堆大小 */
private long heapInitBytes;
/** 最大堆大小 */
private long heapMaxBytes;
/** 已用堆大小 */
private Long heapUsedBytes;
/** 已提交堆大小 */
private Long heapCommittedBytes;
/** GC */
private GC gc;
}
/**
* GC 快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class GC {
/** 收集器名称 */
private String collector;
/** 周期次数 */
private long cycleCount;
/** 暂停次数 */
private long pauseCount;
/** 上次暂停时间 */
private long lastPauseAt;
/** 上次回收大小 */
private long lastRecoveredBytes;
}
/**
* 网络快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Network {
/** 网卡名称 */
private String interfaceName;
/** MAC 地址 */
private String mac;
/** 接收速率 */
private long rxBytesPerSecond;
/** 发送速率 */
private long txBytesPerSecond;
/** 累计接收 */
private long rxTotalBytes;
/** 累计发送 */
private long txTotalBytes;
/** 接收包总数 */
private long rxPacketsTotal;
/** 发送包总数 */
private long txPacketsTotal;
/** 输入错误包总数 */
private long inErrors;
/** 输出错误包总数 */
private long outErrors;
/** 输入丢弃包总数 */
private long inDrops;
/** 碰撞总数 */
private long collisions;
}
/**
* 硬件快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Hardware {
/** 风扇转速 */
private List<Integer> fanSpeeds = new ArrayList<>();
/** 主板信息 */
private Baseboard baseboard;
/** BIOS 信息 */
private Firmware firmware;
}
/**
* 主板快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Baseboard {
/** 厂商 */
private String manufacturer;
/** 型号 */
private String model;
/** 版本 */
private String version;
/** 序列号 */
private String serialNumber;
}
/**
* BIOS 快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Firmware {
/** 厂商 */
private String manufacturer;
/** 名称 */
private String name;
/** 描述 */
private String description;
/** 版本 */
private String version;
/** 发布时间 */
private String releaseDate;
}
/**
* 存储分区快照
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class StoragePartition {
/** 物理磁盘名称 */
private String diskName;
/** 物理磁盘型号 */
private String diskModel;
/** 物理磁盘序列号 */
private String diskSerial;
/** 分区标识 */
private String partitionId;
/** 分区名称 */
private String partitionName;
/** 分区类型 */
private String partitionType;
/** 分区 UUID */
private String uuid;
/** 挂载点 */
private String mountPoint;
/** 分区总空间 */
private long totalBytes;
/** 已用空间 */
private Long usedBytes;
/** 使用率 */
private Double usagePercent;
/** 传输耗时 */
private long transferTimeMs;
/** 健康状态 */
private String healthStatus;
}
}

View File

@@ -0,0 +1,31 @@
package com.imyeyu.api.modules.system.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 系统状态历史视图
*
* @author Codex
* @since 2026-04-06
*/
@Data
public class SystemStatusHistoryView {
/** 服务端当前时间 */
private long serverTime;
/** 采样周期 */
private int sampleRateMs;
/** 起始时间 */
private long from;
/** 结束时间 */
private long to;
/** 历史点 */
private List<SystemStatusDataView.Point> points = new ArrayList<>();
}

View File

@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.system.vo;
import lombok.Data;
/**
* 系统状态快照视图
*
* @author Codex
* @since 2026-04-06
*/
@Data
public class SystemStatusSnapshotView {
/** 服务端当前时间 */
private long serverTime;
/** 采样周期 */
private int sampleRateMs;
/** 当前快照 */
private SystemStatusDataView.Snapshot snapshot = new SystemStatusDataView.Snapshot();
}

View File

@@ -0,0 +1,40 @@
package com.imyeyu.api.modules.system.vo.docker;
import lombok.Data;
/**
* Docker 容器历史点视图
*
* @author Codex
* @since 2026-04-06
*/
@Data
public class DockerContainerHistoryPointView {
/** 时间 */
private long at;
/** CPU 百分比 */
private Double cpuPercent;
/** 内存使用量 */
private Long memoryUsageBytes;
/** 内存百分比 */
private Double memoryPercent;
/** 网络接收字节 */
private Long networkRxBytes;
/** 网络发送字节 */
private Long networkTxBytes;
/** 块设备读取字节 */
private Long blockReadBytes;
/** 块设备写入字节 */
private Long blockWriteBytes;
/** 进程数 */
private Integer pids;
}

View File

@@ -0,0 +1,37 @@
package com.imyeyu.api.modules.system.vo.docker;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* Docker 容器历史视图
*
* @author Codex
* @since 2026-04-06
*/
@Data
public class DockerContainerHistoryView {
/** 容器 ID */
private String id;
/** 容器名称 */
private String name;
/** 服务端当前时间 */
private long serverTime;
/** 采样周期 */
private long sampleRateMs;
/** 起始时间 */
private long from;
/** 结束时间 */
private long to;
/** 历史点 */
private List<DockerContainerHistoryPointView> points = new ArrayList<>();
}

View File

@@ -0,0 +1,82 @@
package com.imyeyu.api.modules.system.vo.docker;
import lombok.Data;
/**
* Docker 容器状态视图
*
* @author Codex
* @since 2026-04-06
*/
@Data
public class DockerContainerStatusView {
/** 容器 ID */
private String id;
/** 容器名称 */
private String name;
/** 镜像 */
private String image;
/** 镜像 ID */
private String imageId;
/** 创建时间 */
private long createdAt;
/** 运行状态 */
private String state;
/** 状态描述 */
private String status;
/** 健康状态 */
private String healthStatus;
/** 启动时间 */
private String startedAt;
/** 结束时间 */
private String finishedAt;
/** 退出码 */
private Integer exitCode;
/** 重启次数 */
private int restartCount;
/** OOM 标记 */
private boolean oomKilled;
/** CPU 百分比 */
private Double cpuPercent;
/** 内存使用量 */
private Long memoryUsageBytes;
/** 内存限制 */
private Long memoryLimitBytes;
/** 内存百分比 */
private Double memoryPercent;
/** 网络接收字节 */
private Long networkRxBytes;
/** 网络发送字节 */
private Long networkTxBytes;
/** 块设备读取字节 */
private Long blockReadBytes;
/** 块设备写入字节 */
private Long blockWriteBytes;
/** 进程数 */
private Integer pids;
/** 更新时间 */
private long updatedAt;
}

View File

@@ -0,0 +1,52 @@
package com.imyeyu.api.modules.system.vo.docker;
import lombok.Data;
/**
* Docker 容器摘要视图
*
* @author Codex
* @since 2026-04-06
*/
@Data
public class DockerContainerSummaryView {
/** 容器 ID */
private String id;
/** 容器名称 */
private String name;
/** 镜像 */
private String image;
/** 运行状态 */
private String state;
/** 状态描述 */
private String status;
/** 健康状态 */
private String healthStatus;
/** CPU 百分比 */
private Double cpuPercent;
/** 内存使用量 */
private Long memoryUsageBytes;
/** 内存限制 */
private Long memoryLimitBytes;
/** 内存百分比 */
private Double memoryPercent;
/** 网络接收字节 */
private Long networkRxBytes;
/** 网络发送字节 */
private Long networkTxBytes;
/** 更新时间 */
private long updatedAt;
}

View File

@@ -0,0 +1,58 @@
package com.imyeyu.api.modules.system.vo.ups;
import lombok.Data;
/**
* UPS 历史点视图
*
* @author Codex
* @since 2026-04-07
*/
@Data
public class UpsHistoryPointView {
/** 采样时间 */
private long at;
/** UPS 数据时间 */
private Long upsTime;
/** 工作模式 */
private String workMode;
/** 输入电压 */
private Double inputVoltage;
/** 输入频率 */
private Double inputFrequency;
/** 输出电压 */
private Double outputVoltage;
/** 输出频率 */
private Double outputFrequency;
/** 输出负载百分比 */
private Integer outputLoadPercent;
/** 电池电压 */
private Double batteryVoltage;
/** 电池容量百分比 */
private Integer batteryCapacity;
/** 电池剩余时间 */
private Integer batteryRemainTime;
/** 温度 */
private Double temperature;
/** 是否旁路运行 */
private boolean bypassActive;
/** 是否关机中 */
private boolean shutdownActive;
/** 是否输出开启 */
private boolean outputOn;
}

View File

@@ -0,0 +1,31 @@
package com.imyeyu.api.modules.system.vo.ups;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* UPS 历史视图
*
* @author Codex
* @since 2026-04-07
*/
@Data
public class UpsHistoryView {
/** 服务端当前时间 */
private long serverTime;
/** 采样周期 */
private long sampleRateMs;
/** 查询开始时间 */
private long from;
/** 查询结束时间 */
private long to;
/** 历史点 */
private List<UpsHistoryPointView> points = new ArrayList<>();
}

View File

@@ -0,0 +1,94 @@
package com.imyeyu.api.modules.system.vo.ups;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* UPS 状态视图
*
* @author Codex
* @since 2026-04-07
*/
@Data
public class UpsStatusView {
/** 服务端当前时间 */
private long serverTime;
/** UPS 数据时间 */
private Long upsTime;
/** 上游主机地址 */
private String hostName;
/** 厂商名称 */
private String customer;
/** 上游版本 */
private String version;
/** 设备标识 */
private String deviceId;
/** UPS 类型 */
private String upsType;
/** UPS 形态 */
private String morphological;
/** 输入输出相位 */
private String ioPhase;
/** 工作模式 */
private String workMode;
/** 输入电压 */
private Double inputVoltage;
/** 输入频率 */
private Double inputFrequency;
/** 输出电压 */
private Double outputVoltage;
/** 输出频率 */
private Double outputFrequency;
/** 输出负载百分比 */
private Integer outputLoadPercent;
/** 电池电压 */
private Double batteryVoltage;
/** 电池容量百分比 */
private Integer batteryCapacity;
/** 电池剩余时间 */
private Integer batteryRemainTime;
/** 温度 */
private Double temperature;
/** 是否旁路运行 */
private boolean bypassActive;
/** 是否关机中 */
private boolean shutdownActive;
/** 是否输出开启 */
private boolean outputOn;
/** 是否充电中 */
private boolean charging;
/** 故障类型 */
private String faultType;
/** 故障明细 */
private String faultKind;
/** 告警列表 */
private List<String> warnings = new ArrayList<>();
}