From 910fe72a1bc0c935d59aefb4763f5d53c049668e Mon Sep 17 00:00:00 2001 From: wxl Date: Tue, 2 Jun 2026 14:42:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=EF=BC=9A?= =?UTF-8?q?=E8=BF=90=E7=BB=B4=E7=9B=91=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lims/entry/CustomMetricConfig.java | 67 +++ .../lims/entry/CustomMetricStat.java | 62 +++ .../org/springblade/lims/LimsApplication.java | 2 +- .../lims/mapper/CustomMetricConfigMapper.java | 14 + .../lims/mapper/CustomMetricStatMapper.java | 14 + .../service/ICustomMetricConfigService.java | 14 + .../service/ICustomMetricStatService.java | 38 ++ .../impl/CustomMetricConfigServiceImpl.java | 20 + .../impl/CustomMetricStatServiceImpl.java | 412 +++++++++++++++ .../CustomMetricConfigController.java | 232 +++++++++ .../monitor/controller/MonitorController.java | 82 +++ .../PersonnelWorkloadController.java | 62 +++ .../scheduled/MonitorScheduledTasks.java | 47 ++ .../monitor/service/MonitorService.java | 50 ++ .../service/PersonnelWorkloadService.java | 28 + .../service/impl/MonitorServiceImpl.java | 488 ++++++++++++++++++ .../impl/PersonnelWorkloadServiceImpl.java | 264 ++++++++++ .../springblade/monitor/util/MonitorUtil.java | 132 +++++ .../springblade/monitor/vo/DiskInfoVO.java | 27 + .../org/springblade/monitor/vo/MonitorVO.java | 44 ++ .../monitor/vo/ServiceStatusVO.java | 25 + .../springblade/monitor/vo/TrendPointVO.java | 21 + .../monitor/vo/WorkloadDetailVO.java | 41 ++ .../monitor/vo/WorkloadSummaryVO.java | 33 ++ 24 files changed, 2218 insertions(+), 1 deletion(-) create mode 100644 lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricConfig.java create mode 100644 lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricStat.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricConfigMapper.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricStatMapper.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricConfigService.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricStatService.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricConfigServiceImpl.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricStatServiceImpl.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/CustomMetricConfigController.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/MonitorController.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/PersonnelWorkloadController.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/scheduled/MonitorScheduledTasks.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/service/MonitorService.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/service/PersonnelWorkloadService.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/MonitorServiceImpl.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/PersonnelWorkloadServiceImpl.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/util/MonitorUtil.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/DiskInfoVO.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/MonitorVO.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/ServiceStatusVO.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/TrendPointVO.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadDetailVO.java create mode 100644 lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadSummaryVO.java diff --git a/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricConfig.java b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricConfig.java new file mode 100644 index 0000000..8cee005 --- /dev/null +++ b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricConfig.java @@ -0,0 +1,67 @@ +package org.springblade.lims.entry; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.mp.base.BaseEntity; + +import java.io.Serializable; + +/** + * 自定义指标配置 + * + * @author swj + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("t_custom_metric_config") +@ApiModel(value = "CustomMetricConfig", description = "自定义指标配置") +public class CustomMetricConfig extends BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 配置名称 + */ + @ApiModelProperty("配置名称") + @JsonProperty("configName") + private String configName; + + /** + * 统计对象(user/dept) + */ + @ApiModelProperty("统计对象(user/dept)") + @JsonProperty("statObject") + private String statObject; + + /** + * 统计字段(detectCount/taskCount/delayCount/reagentUsage) + */ + @ApiModelProperty("统计字段(detectCount/taskCount/delayCount/reagentUsage)") + @JsonProperty("statField") + private String statField; + + /** + * 统计周期(day/week/month/year) + */ + @ApiModelProperty("统计周期(day/week/month/year)") + @JsonProperty("period") + private String period; + + /** + * 聚合方式(sum/avg) + */ + @ApiModelProperty("聚合方式(sum/avg)") + @JsonProperty("aggregation") + private String aggregation; + + /** + * 启用状态(0禁用 1启用) + */ + @ApiModelProperty("启用状态(0禁用 1启用)") + @JsonProperty("enabled") + private Integer enabled; +} diff --git a/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricStat.java b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricStat.java new file mode 100644 index 0000000..df12dc9 --- /dev/null +++ b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricStat.java @@ -0,0 +1,62 @@ +package org.springblade.lims.entry; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.mp.base.BaseEntity; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 自定义指标统计数据 + * + * @author swj + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("t_custom_metric_stat") +@ApiModel(value = "CustomMetricStat", description = "自定义指标统计数据") +public class CustomMetricStat extends BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 配置ID + */ + @ApiModelProperty("配置ID") + @JsonProperty("configId") + private Long configId; + + /** + * 统计日期 + */ + @ApiModelProperty("统计日期") + @JsonProperty("statDate") + private Date statDate; + + /** + * 统计值 + */ + @ApiModelProperty("统计值") + @JsonProperty("statValue") + private BigDecimal statValue; + + /** + * 统计对象ID + */ + @ApiModelProperty("统计对象ID") + @JsonProperty("statObjectId") + private Long statObjectId; + + /** + * 统计对象名称 + */ + @ApiModelProperty("统计对象名称") + @JsonProperty("statObjectName") + private String statObjectName; +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/LimsApplication.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/LimsApplication.java index b7dac47..c82b116 100644 --- a/lab-service/lab-lims/src/main/java/org/springblade/lims/LimsApplication.java +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/LimsApplication.java @@ -16,7 +16,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; */ @EnableBladeFeign @SpringCloudApplication -@ComponentScan({"org.springblade.lims", "org.springblade.stats"}) +@ComponentScan({"org.springblade.lims", "org.springblade.stats", "org.springblade.monitor"}) @EnableScheduling public class LimsApplication { diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricConfigMapper.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricConfigMapper.java new file mode 100644 index 0000000..7922e97 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricConfigMapper.java @@ -0,0 +1,14 @@ +package org.springblade.lims.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.lims.entry.CustomMetricConfig; + +/** + * 自定义指标配置 Mapper + * + * @author swj + * @since 2026-06-01 + */ +public interface CustomMetricConfigMapper extends BaseMapper { + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricStatMapper.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricStatMapper.java new file mode 100644 index 0000000..2430d86 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricStatMapper.java @@ -0,0 +1,14 @@ +package org.springblade.lims.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.lims.entry.CustomMetricStat; + +/** + * 自定义指标统计数据 Mapper + * + * @author swj + * @since 2026-06-01 + */ +public interface CustomMetricStatMapper extends BaseMapper { + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricConfigService.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricConfigService.java new file mode 100644 index 0000000..926b09f --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricConfigService.java @@ -0,0 +1,14 @@ +package org.springblade.lims.service; + +import org.springblade.core.mp.base.BaseService; +import org.springblade.lims.entry.CustomMetricConfig; + +/** + * 自定义指标配置 服务类 + * + * @author swj + * @since 2026-06-01 + */ +public interface ICustomMetricConfigService extends BaseService { + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricStatService.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricStatService.java new file mode 100644 index 0000000..41a1846 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricStatService.java @@ -0,0 +1,38 @@ +package org.springblade.lims.service; + +import org.springblade.core.mp.base.BaseService; +import org.springblade.lims.entry.CustomMetricStat; + +import java.util.Date; +import java.util.Map; + +/** + * 自定义指标统计数据 服务类 + * + * @author swj + * @since 2026-06-01 + */ +public interface ICustomMetricStatService extends BaseService { + + /** + * 执行单个配置的计算 + * + * @param configId 配置ID + * @return 执行结果 + */ + Map executeCalculation(Long configId); + + /** + * 执行所有启用的配置 + */ + void executeAllConfigs(); + + /** + * 删除已有计算结果(幂等性) + * + * @param configId 配置ID + * @param periodStart 周期开始时间 + * @param periodEnd 周期结束时间 + */ + void deleteExistingResults(Long configId, Date periodStart, Date periodEnd); +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricConfigServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricConfigServiceImpl.java new file mode 100644 index 0000000..fbe9c21 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricConfigServiceImpl.java @@ -0,0 +1,20 @@ +package org.springblade.lims.service.impl; + +import lombok.AllArgsConstructor; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.lims.entry.CustomMetricConfig; +import org.springblade.lims.mapper.CustomMetricConfigMapper; +import org.springblade.lims.service.ICustomMetricConfigService; +import org.springframework.stereotype.Service; + +/** + * 自定义指标配置 服务实现类 + * + * @author swj + * @since 2026-06-01 + */ +@Service +@AllArgsConstructor +public class CustomMetricConfigServiceImpl extends BaseServiceImpl implements ICustomMetricConfigService { + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricStatServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricStatServiceImpl.java new file mode 100644 index 0000000..7e6dca0 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricStatServiceImpl.java @@ -0,0 +1,412 @@ +package org.springblade.lims.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.tool.api.R; +import org.springblade.lims.entry.CustomMetricConfig; +import org.springblade.lims.entry.CustomMetricStat; +import org.springblade.lims.entry.Examine; +import org.springblade.lims.entry.ReagentUseLog; +import org.springblade.lims.mapper.CustomMetricStatMapper; +import org.springblade.lims.mapper.ExamineMapper; +import org.springblade.lims.mapper.ReagentUseLogMapper; +import org.springblade.lims.service.ICustomMetricConfigService; +import org.springblade.lims.service.ICustomMetricStatService; +import org.springblade.system.feign.ISysClient; +import org.springblade.system.user.entity.User; +import org.springblade.system.user.feign.IUserClient; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 自定义指标统计数据 服务实现类 + * + * @author swj + * @since 2026-06-01 + */ +@Slf4j +@Service +@AllArgsConstructor +public class CustomMetricStatServiceImpl extends BaseServiceImpl implements ICustomMetricStatService { + + private final ICustomMetricConfigService configService; + private final ExamineMapper examineMapper; + private final ReagentUseLogMapper reagentUseLogMapper; + private final IUserClient userClient; + private final ISysClient sysClient; + + @Override + @Transactional(rollbackFor = Exception.class) + public Map executeCalculation(Long configId) { + // 1. 获取配置 + CustomMetricConfig config = configService.getById(configId); + if (config == null) { + throw new RuntimeException("配置不存在: " + configId); + } + if (!Integer.valueOf(1).equals(config.getEnabled())) { + throw new RuntimeException("配置未启用: " + configId); + } + + // 2. 计算周期起止时间 + Date[] period = calculatePeriod(config.getPeriod()); + Date periodStart = period[0]; + Date periodEnd = period[1]; + + log.info("执行计算 config[{}] statField={} period={} range=[{} - {}]", + configId, config.getStatField(), config.getPeriod(), periodStart, periodEnd); + + // 3. 幂等清理 + deleteExistingResults(configId, periodStart, periodEnd); + + // 4. 计算并保存统计结果 + List results = calculateStats(config, periodStart, periodEnd); + + if (!results.isEmpty()) { + saveBatch(results); + } + + // 5. 返回结果摘要 + Map result = new HashMap<>(8); + result.put("configId", configId); + result.put("configName", config.getConfigName()); + result.put("statField", config.getStatField()); + result.put("period", config.getPeriod()); + result.put("aggregation", config.getAggregation()); + result.put("periodStart", periodStart); + result.put("periodEnd", periodEnd); + result.put("recordCount", results.size()); + return result; + } + + @Override + public void executeAllConfigs() { + List configs = configService.lambdaQuery() + .eq(CustomMetricConfig::getEnabled, 1) + .list(); + log.info("执行所有启用的配置, 共 {} 条", configs.size()); + for (CustomMetricConfig config : configs) { + try { + executeCalculation(config.getId()); + log.info("配置计算完成 [{}]: {}", config.getId(), config.getConfigName()); + } catch (Exception e) { + log.error("配置计算失败 [{}]: {} - {}", config.getId(), config.getConfigName(), e.getMessage(), e); + } + } + } + + @Override + public void deleteExistingResults(Long configId, Date periodStart, Date periodEnd) { + boolean removed = lambdaUpdate() + .eq(CustomMetricStat::getConfigId, configId) + .ge(CustomMetricStat::getStatDate, periodStart) + .le(CustomMetricStat::getStatDate, periodEnd) + .remove(); + if (removed) { + log.info("清除已有结果 configId={}", configId); + } + } + + // ==================== 周期计算 ==================== + + /** + * 根据周期类型计算起止时间 + */ + private Date[] calculatePeriod(String period) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + switch (period) { + case "day": + break; + case "week": + cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + break; + case "month": + cal.set(Calendar.DAY_OF_MONTH, 1); + break; + case "year": + cal.set(Calendar.DAY_OF_YEAR, 1); + break; + default: + throw new RuntimeException("未知统计周期: " + period); + } + + Date periodStart = cal.getTime(); + Date periodEnd = new Date(); + return new Date[]{periodStart, periodEnd}; + } + + // ==================== 统计对象路由 ==================== + + private List calculateStats(CustomMetricConfig config, Date periodStart, Date periodEnd) { + String statObject = config.getStatObject(); + if ("user".equals(statObject)) { + return calculateUserStats(config, periodStart, periodEnd); + } else if ("dept".equals(statObject)) { + return calculateDeptStats(config, periodStart, periodEnd); + } + throw new RuntimeException("未知统计对象: " + statObject); + } + + // ==================== 按用户统计 ==================== + + private List calculateUserStats(CustomMetricConfig config, Date periodStart, Date periodEnd) { + String statField = config.getStatField(); + + if ("reagentUsage".equals(statField)) { + return calculateReagentUsageByUser(config, periodStart, periodEnd); + } + + // 查询指定周期内的 Examine 记录 + LambdaQueryWrapper wrapper = buildExamineQueryWrapper(statField, periodStart, periodEnd); + List examines = examineMapper.selectList(wrapper); + + // 按 examineBy 分组 + Map> byUser = examines.stream() + .filter(e -> e.getExamineBy() != null && !e.getExamineBy().trim().isEmpty()) + .collect(Collectors.groupingBy(Examine::getExamineBy)); + + // 计算每个用户的统计值 + Map userValues = new HashMap<>(byUser.size() * 2); + for (Map.Entry> entry : byUser.entrySet()) { + userValues.put(entry.getKey(), BigDecimal.valueOf(entry.getValue().size())); + } + + return buildStatResults(config, periodStart, userValues); + } + + /** + * 按试剂使用(按用户)统计 + */ + private List calculateReagentUsageByUser(CustomMetricConfig config, Date periodStart, Date periodEnd) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .ge(ReagentUseLog::getCreateTime, periodStart) + .le(ReagentUseLog::getCreateTime, periodEnd); + + List logs = reagentUseLogMapper.selectList(wrapper); + + // 按创建用户分组并汇总用量 + Map userValues = logs.stream() + .filter(l -> l.getCreateUser() != null) + .collect(Collectors.groupingBy( + l -> String.valueOf(l.getCreateUser()), + Collectors.reducing(BigDecimal.ZERO, + l -> BigDecimal.valueOf(l.getCount() != null ? l.getCount() : 0), + BigDecimal::add) + )); + + return buildStatResults(config, periodStart, userValues); + } + + // ==================== 按部门统计 ==================== + + private List calculateDeptStats(CustomMetricConfig config, Date periodStart, Date periodEnd) { + String statField = config.getStatField(); + + if ("reagentUsage".equals(statField)) { + return calculateReagentUsageByDept(config, periodStart, periodEnd); + } + + // 查询指定周期内的 Examine 记录 + LambdaQueryWrapper wrapper = buildExamineQueryWrapper(statField, periodStart, periodEnd); + wrapper.isNotNull(Examine::getDeptId); + List examines = examineMapper.selectList(wrapper); + + // 按 deptId 分组 + Map> byDept = examines.stream() + .filter(e -> e.getDeptId() != null) + .collect(Collectors.groupingBy(Examine::getDeptId)); + + // 计算每个部门的统计值 + Map deptValues = new HashMap<>(byDept.size() * 2); + for (Map.Entry> entry : byDept.entrySet()) { + deptValues.put(String.valueOf(entry.getKey()), BigDecimal.valueOf(entry.getValue().size())); + } + + return buildDeptStatResults(config, periodStart, deptValues); + } + + /** + * 按试剂使用(按部门)统计 + */ + private List calculateReagentUsageByDept(CustomMetricConfig config, Date periodStart, Date periodEnd) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .ge(ReagentUseLog::getCreateTime, periodStart) + .le(ReagentUseLog::getCreateTime, periodEnd); + + List logs = reagentUseLogMapper.selectList(wrapper); + + // 按创建部门分组并汇总用量 + Map deptValues = logs.stream() + .filter(l -> l.getCreateDept() != null) + .collect(Collectors.groupingBy( + l -> String.valueOf(l.getCreateDept()), + Collectors.reducing(BigDecimal.ZERO, + l -> BigDecimal.valueOf(l.getCount() != null ? l.getCount() : 0), + BigDecimal::add) + )); + + return buildDeptStatResults(config, periodStart, deptValues); + } + + // ==================== 查询条件构建 ==================== + + /** + * 根据统计字段类型构建 Examine 查询条件 + */ + private LambdaQueryWrapper buildExamineQueryWrapper(String statField, Date periodStart, Date periodEnd) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + + switch (statField) { + case "detectCount": + // 已完成检测(在周期内完成的) + wrapper.eq(Examine::getIsFinished, "1") + .ge(Examine::getFinishTime, periodStart) + .le(Examine::getFinishTime, periodEnd); + break; + case "taskCount": + // 分配的任务(在周期内领取的) + wrapper.ge(Examine::getReceiveTime, periodStart) + .le(Examine::getReceiveTime, periodEnd); + break; + case "delayCount": + // 超时未完成(要求完成时间在周期内,但未完成) + wrapper.ne(Examine::getIsFinished, "1") + .ge(Examine::getDemandCompletionTime, periodStart) + .le(Examine::getDemandCompletionTime, periodEnd); + break; + default: + throw new RuntimeException("未知统计字段: " + statField); + } + return wrapper; + } + + // ==================== 结果构建 ==================== + + /** + * 按 sum/avg 聚合方式构建用户统计结果 + */ + private List buildStatResults(CustomMetricConfig config, Date statDate, + Map userValues) { + List results = new ArrayList<>(); + + if ("sum".equals(config.getAggregation())) { + // 每人一条记录 + for (Map.Entry entry : userValues.entrySet()) { + String userIdStr = entry.getKey(); + try { + Long userId = Long.parseLong(userIdStr); + String userName = resolveUserName(userId); + results.add(createStatEntity(config.getId(), statDate, entry.getValue(), userId, userName)); + } catch (NumberFormatException e) { + log.warn("跳过无效用户ID: {}", userIdStr); + } + } + } else if ("avg".equals(config.getAggregation())) { + // 计算全员平均值 + if (!userValues.isEmpty()) { + BigDecimal total = userValues.values().stream() + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avg = total.divide(BigDecimal.valueOf(userValues.size()), 2, RoundingMode.HALF_UP); + results.add(createStatEntity(config.getId(), statDate, avg, 0L, "全员平均")); + } + } + + return results; + } + + /** + * 按 sum/avg 聚合方式构建部门统计结果 + */ + private List buildDeptStatResults(CustomMetricConfig config, Date statDate, + Map deptValues) { + List results = new ArrayList<>(); + + if ("sum".equals(config.getAggregation())) { + // 每个部门一条记录 + for (Map.Entry entry : deptValues.entrySet()) { + String deptIdStr = entry.getKey(); + try { + Long deptId = Long.parseLong(deptIdStr); + String deptName = resolveDeptName(deptId); + results.add(createStatEntity(config.getId(), statDate, entry.getValue(), deptId, deptName)); + } catch (NumberFormatException e) { + log.warn("跳过无效部门ID: {}", deptIdStr); + } + } + } else if ("avg".equals(config.getAggregation())) { + // 计算全部门平均值 + if (!deptValues.isEmpty()) { + BigDecimal total = deptValues.values().stream() + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avg = total.divide(BigDecimal.valueOf(deptValues.size()), 2, RoundingMode.HALF_UP); + results.add(createStatEntity(config.getId(), statDate, avg, 0L, "部门平均")); + } + } + + return results; + } + + /** + * 创建统计实体 + */ + private CustomMetricStat createStatEntity(Long configId, Date statDate, BigDecimal statValue, + Long statObjectId, String statObjectName) { + CustomMetricStat stat = new CustomMetricStat(); + stat.setConfigId(configId); + stat.setStatDate(statDate); + stat.setStatValue(statValue); + stat.setStatObjectId(statObjectId); + stat.setStatObjectName(statObjectName); + return stat; + } + + // ==================== 名称解析 ==================== + + /** + * 根据用户ID查询用户名 + */ + private String resolveUserName(Long userId) { + try { + R result = userClient.userInfoById(userId); + if (result.isSuccess() && result.getData() != null) { + String name = result.getData().getName(); + String realName = result.getData().getRealName(); + return realName != null && !realName.trim().isEmpty() ? realName : name; + } + } catch (Exception e) { + log.warn("查询用户名称失败 userId={}: {}", userId, e.getMessage()); + } + return String.valueOf(userId); + } + + /** + * 根据部门ID查询部门名称 + */ + private String resolveDeptName(Long deptId) { + try { + R result = sysClient.getDeptName(deptId); + if (result.isSuccess() && result.getData() != null) { + return result.getData(); + } + } catch (Exception e) { + log.warn("查询部门名称失败 deptId={}: {}", deptId, e.getMessage()); + } + return String.valueOf(deptId); + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/CustomMetricConfigController.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/CustomMetricConfigController.java new file mode 100644 index 0000000..e86fc91 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/CustomMetricConfigController.java @@ -0,0 +1,232 @@ +package org.springblade.monitor.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.lims.entry.CustomMetricConfig; +import org.springblade.lims.entry.CustomMetricStat; +import org.springblade.lims.service.ICustomMetricConfigService; +import org.springblade.lims.service.ICustomMetricStatService; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 自定义指标配置 控制器 + * + * @author swj + * @since 2026-06-01 + */ +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping("/monitor/metric-config") +@Api(value = "自定义指标配置", tags = "自定义指标配置") +public class CustomMetricConfigController extends BladeController { + + private final ICustomMetricConfigService service; + + private final ICustomMetricStatService customMetricStatService; + + /** + * 分页列表 + */ + @GetMapping("list") + @ApiOperationSupport(order = 2) + @ApiOperation(value = "分页列表", notes = "分页列表") + public R> list(CustomMetricConfig entry, Query query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (entry.getConfigName() != null) { + wrapper.like(CustomMetricConfig::getConfigName, entry.getConfigName()); + } + wrapper.orderByDesc(CustomMetricConfig::getCreateTime); + IPage page = service.page(Condition.getPage(query), wrapper); + return R.data(page); + } + + /** + * 新增 + */ + @PostMapping("save") + @ApiOperation(value = "新增", notes = "新增") + public R save(@RequestBody CustomMetricConfig entry) { + entry.setCreateTime(new Date()); + entry.setUpdateTime(new Date()); + return R.status(service.save(entry)); + } + + /** + * 修改 + */ + @PostMapping("update") + @ApiOperation(value = "修改", notes = "修改") + public R update(@RequestBody CustomMetricConfig entry) { + return R.status(service.updateById(entry)); + } + + /** + * 逻辑删除 + */ + @PostMapping("deleteByIds") + @ApiOperation(value = "逻辑删除", notes = "传入ids") + public R deleteByIds(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) { + return R.status(service.deleteLogic(Func.toLongList(ids))); + } + + /** + * 手动触发统计计算 + *

+ * 跳过 DictBizCache 定时开关判断,直接执行计算。 + * 可选传入 configId 执行单个配置,为空则执行所有启用的配置。 + *

+ */ + @PostMapping("trigger-calculation") + @ApiOperation(value = "手动触发计算", notes = "手动触发自定义指标统计计算,跳过定时开关判断。可选指定配置ID,为空则执行所有启用配置。") + public R> triggerCalculation( + @ApiParam(value = "配置ID,为空则执行所有启用配置") + @RequestParam(required = false) Long configId) { + // Admin-only auth check +// if (!AuthUtil.isAdministrator()) { +// return R.fail("仅管理员可执行此操作"); +// } + + List> details = new ArrayList<>(); + int successCount = 0; + int failCount = 0; + + if (configId != null) { + // 执行单个配置 + try { + Map detail = customMetricStatService.executeCalculation(configId); + detail.put("status", "success"); + successCount = 1; + details.add(detail); + } catch (Exception e) { + failCount = 1; + Map errorDetail = new HashMap<>(4); + errorDetail.put("configId", configId); + errorDetail.put("status", "fail"); + errorDetail.put("error", e.getMessage()); + details.add(errorDetail); + log.error("手动触发计算失败 configId={}", configId, e); + } + } else { + // 执行所有启用的配置 + List configs = service.lambdaQuery() + .eq(CustomMetricConfig::getEnabled, 1) + .list(); + for (CustomMetricConfig config : configs) { + try { + Map detail = customMetricStatService.executeCalculation(config.getId()); + detail.put("status", "success"); + successCount++; + details.add(detail); + } catch (Exception e) { + failCount++; + Map errorDetail = new HashMap<>(4); + errorDetail.put("configId", config.getId()); + errorDetail.put("configName", config.getConfigName()); + errorDetail.put("status", "fail"); + errorDetail.put("error", e.getMessage()); + details.add(errorDetail); + log.error("手动触发计算失败 configId={} configName={}", config.getId(), config.getConfigName(), e); + } + } + } + + Map result = new HashMap<>(8); + result.put("successCount", successCount); + result.put("failCount", failCount); + result.put("total", successCount + failCount); + result.put("details", details); + return R.data(result); + } + + /** + * 统计结果分页列表 + *

+ * 分页查询自定义指标统计结果,附带配置名称。 + *

+ */ + @GetMapping("metric-stat/list") + @ApiOperationSupport(order = 5) + @ApiOperation(value = "统计结果分页列表", notes = "分页查询自定义指标统计结果") + public R> metricStatList( + @ApiParam(value = "配置ID") @RequestParam(required = false) Long configId, + @ApiParam(value = "开始日期(yyyy-MM-dd)") @RequestParam(required = false) String startDateStr, + @ApiParam(value = "结束日期(yyyy-MM-dd)") @RequestParam(required = false) String endDateStr, + @ApiParam(value = "统计对象ID") @RequestParam(required = false) Long statObjectId, + Query query) { + Date startDate = parseDateStr(startDateStr); + Date endDate = parseDateStr(endDateStr); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (configId != null) { + wrapper.eq(CustomMetricStat::getConfigId, configId); + } + if (startDate != null) { + wrapper.ge(CustomMetricStat::getStatDate, startDate); + } + if (endDate != null) { + wrapper.le(CustomMetricStat::getStatDate, endDate); + } + if (statObjectId != null) { + wrapper.eq(CustomMetricStat::getStatObjectId, statObjectId); + } + wrapper.orderByDesc(CustomMetricStat::getCreateTime); + IPage page = customMetricStatService.page(Condition.getPage(query), wrapper); + + // Enrich records with config name + List> records = new ArrayList<>(page.getRecords().size()); + for (CustomMetricStat stat : page.getRecords()) { + Map record = new HashMap<>(8); + record.put("id", stat.getId()); + record.put("configId", stat.getConfigId()); + if (stat.getConfigId() != null) { + CustomMetricConfig config = service.getById(stat.getConfigId()); + record.put("configName", config != null ? config.getConfigName() : ""); + } else { + record.put("configName", ""); + } + record.put("statDate", stat.getStatDate()); + record.put("statValue", stat.getStatValue()); + record.put("statObjectId", stat.getStatObjectId()); + record.put("statObjectName", stat.getStatObjectName()); + record.put("createTime", stat.getCreateTime()); + records.add(record); + } + + Map result = new HashMap<>(8); + result.put("records", records); + result.put("total", page.getTotal()); + result.put("size", page.getSize()); + result.put("current", page.getCurrent()); + return R.data(result); + } + + /** + * 将 yyyy-MM-dd 字符串解析为 Date,空字符串或 null 返回 null + */ + private static Date parseDateStr(String dateStr) { + if (dateStr == null || dateStr.trim().isEmpty()) { + return null; + } + return DateUtil.parse(dateStr, DateUtil.PATTERN_DATE); + } + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/MonitorController.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/MonitorController.java new file mode 100644 index 0000000..5ac4dda --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/MonitorController.java @@ -0,0 +1,82 @@ +package org.springblade.monitor.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.tool.api.R; +import org.springblade.monitor.service.MonitorService; +import org.springblade.monitor.vo.MonitorVO; +import org.springblade.monitor.vo.ServiceStatusVO; +import org.springblade.monitor.vo.TrendPointVO; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 系统监控 控制器 + * + * @author blade + * @since 2026-06-01 + */ +@RestController +@AllArgsConstructor +@RequestMapping("/monitor") +@Api(value = "系统监控", tags = "系统监控") +public class MonitorController extends BladeController { + + private final MonitorService monitorService; + + /** + * 系统监控指标 + *

+ * 获取系统 CPU、JVM 内存、物理内存、磁盘使用率、网络延迟等指标。 + * 每次调用会自动更新趋势数据缓冲区。 + *

+ */ + @GetMapping("/system") + @ApiOperation(value = "系统监控指标", notes = "获取系统 CPU、内存、磁盘等指标,同时更新趋势缓冲区") + public R system() { + MonitorVO metrics = monitorService.getSystemMetrics(); + return R.data(metrics); + } + + /** + * 服务健康状态列表 + *

+ * 检查 Java 应用、MySQL、Redis 等服务健康状态。 + * 每次调用前会自动采集系统指标以更新趋势缓冲区。 + *

+ */ + @GetMapping("/services") + @ApiOperation(value = "服务健康状态", notes = "检查 Java、MySQL、Redis 等服务健康状态") + public R> services() { + // 采集系统指标以更新趋势缓冲区 + monitorService.getSystemMetrics(); + List statusList = monitorService.getServiceStatus(); + return R.data(statusList); + } + + /** + * 趋势数据 + *

+ * 获取指定指标的最近趋势数据。 + * type 可选值: cpu, memory, disk, network + * 每次调用前会自动采集系统指标以更新趋势缓冲区。 + *

+ */ + @GetMapping("/trend") + @ApiOperation(value = "趋势数据", notes = "获取指定指标的最近趋势数据,type 可选值: cpu / memory / disk / network") + public R> trend( + @ApiParam(value = "指标类型 (cpu / memory / disk / network)", required = true) + @RequestParam String type, + @ApiParam(value = "返回数据点数,默认 60") + @RequestParam(defaultValue = "60") int points) { + // 采集系统指标以更新趋势缓冲区 + monitorService.getSystemMetrics(); + List trend = monitorService.getTrend(type, points); + return R.data(trend); + } + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/PersonnelWorkloadController.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/PersonnelWorkloadController.java new file mode 100644 index 0000000..2c19cda --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/PersonnelWorkloadController.java @@ -0,0 +1,62 @@ +package org.springblade.monitor.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.monitor.service.PersonnelWorkloadService; +import org.springblade.monitor.vo.WorkloadSummaryVO; +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; + +import java.util.Date; + +/** + * 人员工作量统计 控制器 + * + * @author blade + * @since 2026-06-01 + */ +@RestController +@AllArgsConstructor +@RequestMapping("/monitor") +@Api(value = "人员工作量统计", tags = "人员工作量统计") +public class PersonnelWorkloadController extends BladeController { + + private final PersonnelWorkloadService personnelWorkloadService; + + /** + * 获取人员工作量统计 + * + * @param start 开始日期 (yyyy-MM-dd),可选 + * @param end 结束日期 (yyyy-MM-dd),可选 + * @param deptId 部门 ID,可选 + * @return 工作量汇总数据 + */ + @GetMapping("workload") + @ApiOperation(value = "人员工作量统计", notes = "按人员维度统计检验工作量,包括完成率、延期率、平均耗时") + public R workload( + @ApiParam(value = "开始日期(yyyy-MM-dd)") @RequestParam(required = false) String startStr, + @ApiParam(value = "结束日期(yyyy-MM-dd)") @RequestParam(required = false) String endStr, + @ApiParam(value = "部门ID") @RequestParam(required = false) Long deptId) { + Date start = parseDate(startStr); + Date end = parseDate(endStr); + WorkloadSummaryVO result = personnelWorkloadService.getWorkload(start, end, deptId); + return R.data(result); + } + + /** + * 将 yyyy-MM-dd 字符串解析为 Date,空字符串或 null 返回 null + */ + private static Date parseDate(String dateStr) { + if (dateStr == null || dateStr.trim().isEmpty()) { + return null; + } + return DateUtil.parse(dateStr, DateUtil.PATTERN_DATE); + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/scheduled/MonitorScheduledTasks.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/scheduled/MonitorScheduledTasks.java new file mode 100644 index 0000000..a190197 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/scheduled/MonitorScheduledTasks.java @@ -0,0 +1,47 @@ + +package org.springblade.monitor.scheduled; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.lims.service.ICustomMetricStatService; +import org.springblade.system.cache.DictBizCache; +import org.springblade.system.enums.DictBizEnum; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 监控模块定时任务 + * + * @author swj + * @since 2026-06-01 + */ +@Slf4j +@Component +@AllArgsConstructor +public class MonitorScheduledTasks { + + private final ICustomMetricStatService customMetricStatService; + + /** + * 每晚 00:00 执行自定义指标统计 + */ + @Scheduled(cron = "0 0 0 * * ?") + public void executeMetricStatAtMidnight() { + // ===================== 核心:开关判断 ===================== + String enabled = DictBizCache.getKey(DictBizEnum.SCHEDULED_TASK.getName(), "openMonitorScheduled"); + if ("1".equals(enabled)) { + log.info("===== 指标统计定时任务已关闭,不执行 ====="); + return; + } + log.info("===== 凌晨0点,开始执行指标统计任务 =====: {}", LocalDateTime.now()); + try { + customMetricStatService.executeAllConfigs(); + log.info("===== 指标统计任务完成 =====: {}", LocalDateTime.now()); + } catch (Exception e) { + log.error("===== 指标统计任务异常 =====: {}", LocalDateTime.now(), e); + } + } + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/MonitorService.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/MonitorService.java new file mode 100644 index 0000000..22b382d --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/MonitorService.java @@ -0,0 +1,50 @@ +package org.springblade.monitor.service; + +import org.springblade.monitor.vo.MonitorVO; +import org.springblade.monitor.vo.ServiceStatusVO; +import org.springblade.monitor.vo.TrendPointVO; + +import java.util.List; + +/** + * 系统监控服务接口 + *

+ * 提供系统指标采集、服务健康检查、网络延迟测量、趋势数据查询功能。 + * 所有数据均通过 JDK 内置 API 采集,无需第三方依赖。 + *

+ * + * @author blade + * @since 2026-06-01 + */ +public interface MonitorService { + + /** + * 获取系统监控指标(CPU、内存、磁盘) + * + * @return MonitorVO 包含完整的系统资源指标 + */ + MonitorVO getSystemMetrics(); + + /** + * 检查各服务健康状态 + * + * @return 服务状态列表(Java 应用 / MySQL / Redis 等) + */ + List getServiceStatus(); + + /** + * 测量到目标主机的网络延迟 + * + * @return 延迟 (ms),失败时返回 -1 + */ + Long getNetworkLatency(); + + /** + * 获取指定指标的最近趋势数据 + * + * @param type 指标类型 (cpu / memory / disk / network) + * @param points 返回的数据点数 + * @return 趋势数据点列表 + */ + List getTrend(String type, int points); +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/PersonnelWorkloadService.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/PersonnelWorkloadService.java new file mode 100644 index 0000000..9bc79e7 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/PersonnelWorkloadService.java @@ -0,0 +1,28 @@ +package org.springblade.monitor.service; + +import org.springblade.monitor.vo.WorkloadSummaryVO; + +import java.util.Date; + +/** + * 人员工作量统计服务接口 + *

+ * 基于 Examine 检验记录,按人员维度统计工作量、完成率、延期率、平均耗时等指标。 + * 跨数据源查询:Examine (sxm_system) + blade_user / blade_dept (sxmlims)。 + *

+ * + * @author blade + * @since 2026-06-01 + */ +public interface PersonnelWorkloadService { + + /** + * 获取人员工作量统计 + * + * @param startDate 开始日期(基于 receiveTime 筛选,可选) + * @param endDate 结束日期(可选) + * @param deptId 部门 ID(可选) + * @return 工作量汇总 VO + */ + WorkloadSummaryVO getWorkload(Date startDate, Date endDate, Long deptId); +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/MonitorServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/MonitorServiceImpl.java new file mode 100644 index 0000000..0f6414c --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/MonitorServiceImpl.java @@ -0,0 +1,488 @@ +package org.springblade.monitor.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.monitor.service.MonitorService; +import org.springblade.monitor.util.MonitorUtil; +import org.springblade.monitor.vo.DiskInfoVO; +import org.springblade.monitor.vo.MonitorVO; +import org.springblade.monitor.vo.ServiceStatusVO; +import org.springblade.monitor.vo.TrendPointVO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import javax.sql.DataSource; +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 系统监控服务实现 + *

+ * 使用 JDK 内置 API 采集系统指标,通过 HttpURLConnection / DataSource / RedisTemplate + * 进行健康检查,通过 Socket 测量网络延迟,使用 ConcurrentLinkedQueue 维护趋势数据缓冲区。 + *

+ * + * @author blade + * @since 2026-06-01 + */ +@Slf4j +@Service +public class MonitorServiceImpl implements MonitorService { + + private static final int TREND_BUFFER_CAPACITY = 1000; + + private final DataSource dataSource; + private final RedisTemplate redisTemplate; + + public MonitorServiceImpl(DataSource dataSource, RedisTemplate redisTemplate) { + this.dataSource = dataSource; + this.redisTemplate = redisTemplate; + } + + /** + * 多个 Java 应用地址列表(逗号分隔),未配置时通过系统调用检测本地 Java 进程 + */ + @Value("${monitor.health.java.urls:}") + private String javaHealthUrls; + + @Value("${monitor.network.target:localhost:83}") + private String networkTarget; + + /** + * 趋势数据缓冲区 — 线程安全,最多保留 TREND_BUFFER_CAPACITY 条记录。 + * 每条记录为 Map,包含字段: timestamp, cpu, memory, disk, network。 + */ + private final ConcurrentLinkedQueue> trendBuffer = new ConcurrentLinkedQueue<>(); + + // ==================== Public API ==================== + + @Override + public MonitorVO getSystemMetrics() { + MonitorVO vo = new MonitorVO(); + + // --- CPU --- + vo.setCpuUsage(MonitorUtil.getCpuUsage()); + vo.setCpuCores(MonitorUtil.getAvailableProcessors()); + + // --- JVM Memory --- + long jvmTotal = Runtime.getRuntime().totalMemory(); + long jvmFree = Runtime.getRuntime().freeMemory(); + long jvmMax = Runtime.getRuntime().maxMemory(); + vo.setMemoryTotal(jvmTotal); + vo.setMemoryFree(jvmFree); + vo.setMemoryUsed(jvmTotal - jvmFree); + vo.setMemoryUsage(jvmTotal > 0 ? (double) (jvmTotal - jvmFree) / jvmTotal * 100 : 0); + + // --- Physical Memory --- + long physTotal = MonitorUtil.getTotalPhysicalMemory(); + long physFree = MonitorUtil.getFreePhysicalMemory(); + vo.setPhysicalMemoryTotal(physTotal); + vo.setPhysicalMemoryFree(physFree); + vo.setPhysicalMemoryUsage(physTotal > 0 ? (double) (physTotal - physFree) / physTotal * 100 : 0); + + // --- Disk --- + List diskList = new ArrayList<>(); + for (File root : File.listRoots()) { + DiskInfoVO disk = new DiskInfoVO(); + disk.setPath(root.getAbsolutePath()); + long total = root.getTotalSpace(); + long free = root.getFreeSpace(); + long used = total - free; + disk.setTotalSpace(total); + disk.setFreeSpace(free); + disk.setUsedSpace(used); + disk.setUsagePercent(total > 0 ? (double) used / total * 100 : 0); + diskList.add(disk); + } + vo.setDiskList(diskList); + + // --- Network Latency --- + vo.setNetworkLatencyMs(getNetworkLatency()); + + // --- Push to trend buffer --- + pushTrendSnapshot(vo); + + return vo; + } + + @Override + public List getServiceStatus() { + List statusList = new ArrayList<>(); + + statusList.addAll(checkJavaServices()); + statusList.add(checkMysqlService()); + statusList.add(checkRedisService()); + + return statusList; + } + + @Override + public Long getNetworkLatency() { + String host = "localhost"; + int port = 8080; + + // Parse host:port from config + String target = networkTarget.trim(); + int colonIdx = target.lastIndexOf(':'); + if (colonIdx > 0) { + host = target.substring(0, colonIdx); + try { + port = Integer.parseInt(target.substring(colonIdx + 1)); + } catch (NumberFormatException e) { + log.warn("Invalid port in network target '{}', using default 83", target); + port = 83; + } + } else { + host = target; + } + + long start = System.currentTimeMillis(); + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(host, port), 3000); + return System.currentTimeMillis() - start; + } catch (Exception e) { + log.debug("Network latency check failed for {}:{} - {}", host, port, e.getMessage()); + return -1L; + } + } + + @Override + public List getTrend(String type, int points) { + List result = new ArrayList<>(); + int count = 0; + + // Iterate from newest to oldest (reverse order) + Object[] snapshotArray = trendBuffer.toArray(); + for (int i = snapshotArray.length - 1; i >= 0 && count < points; i--) { + @SuppressWarnings("unchecked") + Map snapshot = (Map) snapshotArray[i]; + TrendPointVO point = new TrendPointVO(); + point.setTimestamp((Long) snapshot.get("timestamp")); + + Object value = snapshot.get(type); + if (value instanceof Number) { + point.setValue(((Number) value).doubleValue()); + result.add(0, point); // prepend to maintain chronological order + count++; + } + } + + return result; + } + + // ==================== Health Checks ==================== + + /** + * 检查 Java 应用健康状态 + *

+ * 若配置了 monitor.health.java.urls(逗号分隔),对每个 URL 进行 HTTP 健康检查; + * 否则通过系统调用(jps / wmic)检测本地运行的 Java 进程。 + *

+ */ + private List checkJavaServices() { + String[] urls = parseJavaHealthUrls(); + if (urls.length > 0) { + return checkRemoteJavaServices(urls); + } + return detectLocalJavaProcesses(); + } + + /** + * 对配置的 URL 列表进行 HTTP 健康检查 + */ + private List checkRemoteJavaServices(String[] urls) { + List results = new ArrayList<>(); + for (String url : urls) { + String trimmed = url.trim(); + if (!trimmed.isEmpty()) { + results.add(checkSingleJavaService(trimmed, extractAppName(trimmed))); + } + } + return results; + } + + /** + * 通过系统调用检测本地 Java 进程 + *

+ * 优先使用 JDK 自带的 jps -l 命令(显示主类名), + * 若不可用则降级为 wmic 命令。 + *

+ */ + private List detectLocalJavaProcesses() { + List results = new ArrayList<>(); + long start = System.currentTimeMillis(); + + // 优先 jps -l + boolean jpsSuccess = tryJpsDetection(results, start); + + // jps 失败则尝试 wmic + if (!jpsSuccess) { + tryWmicDetection(results, start); + } + + // 未检测到任何 Java 进程 + if (results.isEmpty()) { + ServiceStatusVO status = new ServiceStatusVO(); + status.setName("Java Application"); + status.setStatus("DOWN"); + status.setLatencyMs(System.currentTimeMillis() - start); + status.setErrorMessage("No local Java processes detected"); + results.add(status); + } + + return results; + } + + /** + * 使用 jps -l 检测本地 Java 进程 + * + * @return true 表示检测成功且有结果 + */ + private boolean tryJpsDetection(List results, long start) { + try { + Process process = Runtime.getRuntime().exec(new String[]{"jps", "-l"}); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), Charset.forName("GBK")))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + String[] parts = line.split("\\s+", 2); + ServiceStatusVO status = new ServiceStatusVO(); + status.setStatus("UP"); + status.setLatencyMs(System.currentTimeMillis() - start); + if (parts.length >= 2) { + String mainClass = parts[1]; + String shortName = mainClass.substring(mainClass.lastIndexOf('.') + 1); + status.setName("Java (" + shortName + ")"); + status.setErrorMessage("PID: " + parts[0] + ", " + mainClass); + } else { + status.setName("Java (PID:" + parts[0] + ")"); + status.setErrorMessage("PID: " + parts[0]); + } + results.add(status); + } + } + int exitCode = process.waitFor(); + return exitCode == 0 && !results.isEmpty(); + } catch (Exception e) { + log.debug("jps detection failed: {}", e.getMessage()); + return false; + } + } + + /** + * 使用 wmic 检测本地 Java 进程(jps 不可用时的降级方案) + */ + private void tryWmicDetection(List results, long start) { + try { + Process process = Runtime.getRuntime().exec( + new String[]{"wmic", "process", "where", "name='java.exe'", "get", "ProcessId,CommandLine", "/FORMAT:CSV"}); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), Charset.forName("GBK")))) { + String line; + boolean header = true; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + if (header) { + header = false; + continue; + } + // CSV format: Node,CommandLine,ProcessId + String[] csvParts = line.split(","); + if (csvParts.length >= 2) { + ServiceStatusVO status = new ServiceStatusVO(); + String cmdLine = csvParts[1].replace("\"", ""); + String appName = extractJavaAppName(cmdLine); + status.setName("Java (" + appName + ")"); + status.setStatus("UP"); + status.setLatencyMs(System.currentTimeMillis() - start); + String pid = csvParts.length >= 3 ? csvParts[2].replace("\"", "") : "?"; + status.setErrorMessage("PID: " + pid); + results.add(status); + } + } + } + } catch (Exception e) { + log.debug("wmic detection failed: {}", e.getMessage()); + } + } + + /** + * 检查单个 Java 应用 HTTP 健康状态 + */ + private ServiceStatusVO checkSingleJavaService(String url, String displayName) { + ServiceStatusVO status = new ServiceStatusVO(); + status.setName(displayName); + long start = System.currentTimeMillis(); + + try { + URL healthUrl = new URL(url); + HttpURLConnection conn = (HttpURLConnection) healthUrl.openConnection(); + conn.setConnectTimeout(3000); + conn.setReadTimeout(3000); + conn.setRequestMethod("GET"); + int responseCode = conn.getResponseCode(); + + status.setLatencyMs(System.currentTimeMillis() - start); + if (responseCode == 200) { + status.setStatus("UP"); + } else { + status.setStatus("DOWN"); + status.setErrorMessage("HTTP " + responseCode); + } + conn.disconnect(); + } catch (Exception e) { + status.setLatencyMs(System.currentTimeMillis() - start); + status.setStatus("DOWN"); + status.setErrorMessage(e.getMessage()); + log.debug("Java health check failed for {}: {}", url, e.getMessage()); + } + + return status; + } + + /** + * 解析逗号分隔的 Java 应用地址列表 + */ + private String[] parseJavaHealthUrls() { + if (javaHealthUrls != null && !javaHealthUrls.trim().isEmpty()) { + return javaHealthUrls.split(","); + } + return new String[0]; + } + + /** + * 从 URL 中提取应用显示名称(取 host:port) + */ + private String extractAppName(String url) { + try { + URL parsed = new URL(url); + return "Java Application (" + parsed.getHost() + ":" + parsed.getPort() + ")"; + } catch (Exception e) { + return "Java Application (" + url + ")"; + } + } + + /** + * 从 wmic 命令行中提取 Java 应用名称 + */ + private String extractJavaAppName(String cmdLine) { + if (cmdLine == null || cmdLine.isEmpty()) { + return "Unknown"; + } + // -jar 方式启动 → 提取 jar 文件名 + int jarIdx = cmdLine.indexOf("-jar"); + if (jarIdx >= 0) { + String afterJar = cmdLine.substring(jarIdx + 4).trim(); + String jarFile = afterJar.split("\\s+")[0]; + int lastSep = jarFile.replace('\\', '/').lastIndexOf('/'); + return lastSep >= 0 ? jarFile.substring(lastSep + 1) : jarFile; + } + // 主类方式启动 → 提取类名简称 + String[] parts = cmdLine.split("\\s+"); + for (int i = parts.length - 1; i >= 0; i--) { + String p = parts[i].trim(); + if (p.contains(".") && !p.startsWith("-") && !p.startsWith("-D") && !p.startsWith("-X")) { + return p.substring(p.lastIndexOf('.') + 1); + } + } + return "Java Process"; + } + + /** + * 检查 MySQL 数据库连接状态 + */ + private ServiceStatusVO checkMysqlService() { + ServiceStatusVO status = new ServiceStatusVO(); + status.setName("MySQL"); + long start = System.currentTimeMillis(); + + try (java.sql.Connection conn = dataSource.getConnection()) { + boolean valid = conn.isValid(5); + status.setLatencyMs(System.currentTimeMillis() - start); + if (valid) { + status.setStatus("UP"); + } else { + status.setStatus("DOWN"); + status.setErrorMessage("Connection validation returned false"); + } + } catch (Exception e) { + status.setLatencyMs(System.currentTimeMillis() - start); + status.setStatus("DOWN"); + status.setErrorMessage(e.getMessage()); + log.debug("MySQL health check failed: {}", e.getMessage()); + } + + return status; + } + + /** + * 检查 Redis 连接状态 + */ + private ServiceStatusVO checkRedisService() { + ServiceStatusVO status = new ServiceStatusVO(); + status.setName("Redis"); + long start = System.currentTimeMillis(); + + try { + String pingResult = redisTemplate.getConnectionFactory().getConnection().ping(); + status.setLatencyMs(System.currentTimeMillis() - start); + if ("PONG".equalsIgnoreCase(pingResult)) { + status.setStatus("UP"); + } else { + status.setStatus("DOWN"); + status.setErrorMessage("Unexpected ping response: " + pingResult); + } + } catch (Exception e) { + status.setLatencyMs(System.currentTimeMillis() - start); + status.setStatus("DOWN"); + status.setErrorMessage(e.getMessage()); + log.debug("Redis health check failed: {}", e.getMessage()); + } + + return status; + } + + // ==================== Trend Buffer ==================== + + /** + * 将当前快照推入趋势缓冲区,超过容量时移除最旧记录 + */ + private void pushTrendSnapshot(MonitorVO vo) { + Map snapshot = new LinkedHashMap<>(); + snapshot.put("timestamp", System.currentTimeMillis()); + snapshot.put("cpu", vo.getCpuUsage()); + snapshot.put("memory", vo.getMemoryUsage()); + // 磁盘使用率取第一个磁盘的使用率作为整体指标 + double diskUsage = 0; + if (vo.getDiskList() != null && !vo.getDiskList().isEmpty()) { + diskUsage = vo.getDiskList().get(0).getUsagePercent(); + } + snapshot.put("disk", diskUsage); + snapshot.put("network", (double) vo.getNetworkLatencyMs()); + + trendBuffer.offer(snapshot); + + // 超过容量时移除最旧记录 + while (trendBuffer.size() > TREND_BUFFER_CAPACITY) { + trendBuffer.poll(); + } + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/PersonnelWorkloadServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/PersonnelWorkloadServiceImpl.java new file mode 100644 index 0000000..4887d38 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/PersonnelWorkloadServiceImpl.java @@ -0,0 +1,264 @@ +package org.springblade.monitor.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.api.R; +import org.springblade.lims.entry.Examine; +import org.springblade.lims.mapper.ExamineMapper; +import org.springblade.monitor.service.PersonnelWorkloadService; +import org.springblade.monitor.vo.WorkloadDetailVO; +import org.springblade.monitor.vo.WorkloadSummaryVO; +import org.springblade.system.entity.Dept; +import org.springblade.system.feign.ISysClient; +import org.springblade.system.user.entity.User; +import org.springblade.system.user.feign.IUserClient; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 人员工作量统计服务实现 + *

+ * 基于 Examine 检验记录,按人员维度统计工作量。 + * 跨数据源通过 Feign 客户端查询 blade_user / blade_dept。 + *

+ * + * @author blade + * @since 2026-06-01 + */ +@Slf4j +@Service +@AllArgsConstructor +public class PersonnelWorkloadServiceImpl implements PersonnelWorkloadService { + + private final ExamineMapper examineMapper; + private final IUserClient userClient; + private final ISysClient sysClient; + + @Override + public WorkloadSummaryVO getWorkload(Date startDate, Date endDate, Long deptId) { + // 1. 查询 Examine 记录 + List examineList = queryExamineRecords(startDate, endDate, deptId); + + // 2. 按 examineBy 分组 + Map> groupByUser = examineList.stream() + .filter(e -> e.getExamineBy() != null && !e.getExamineBy().isEmpty()) + .collect(Collectors.groupingBy(Examine::getExamineBy)); + + // 3. 构建明细 + List details = new ArrayList<>(); + int totalDetectionCount = 0; + BigDecimal sumDelayRate = BigDecimal.ZERO; + + for (Map.Entry> entry : groupByUser.entrySet()) { + WorkloadDetailVO detail = buildDetail(entry.getKey(), entry.getValue()); + details.add(detail); + totalDetectionCount += detail.getDetectionCount(); + sumDelayRate = sumDelayRate.add(detail.getDelayRate()); + } + + // 4. 构建汇总 + return buildSummary(details, totalDetectionCount, sumDelayRate, startDate, endDate, examineList); + } + + /** + * 查询指定时间范围内的 Examine 记录 + */ + private List queryExamineRecords(Date startDate, Date endDate, Long deptId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (startDate != null) { + wrapper.ge(Examine::getReceiveTime, startDate); + } + if (endDate != null) { + wrapper.le(Examine::getReceiveTime, endDate); + } + if (deptId != null) { + wrapper.eq(Examine::getDeptId, deptId); + } + // 排除未分配人员的记录 + wrapper.isNotNull(Examine::getExamineBy); + wrapper.ne(Examine::getExamineBy, ""); + + return examineMapper.selectList(wrapper); + } + + /** + * 构建单个人员的工作量明细 + */ + private WorkloadDetailVO buildDetail(String userId, List records) { + Date now = new Date(); + + int detectionCount = records.size(); + int completedCount = (int) records.stream() + .filter(e -> "1".equals(e.getIsFinished())) + .count(); + int delayCount = (int) records.stream() + .filter(e -> !"1".equals(e.getIsFinished()) + && e.getDemandCompletionTime() != null + && e.getDemandCompletionTime().before(now)) + .count(); + + // 延期率 = delayCount / detectionCount * 100 + BigDecimal delayRate = detectionCount > 0 + ? BigDecimal.valueOf(delayCount) + .multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(detectionCount), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + + // 平均耗时 + String avgTime = calculateAvgTime(records); + + // 用户真实姓名 + String realName = resolveUserName(userId); + + // 部门名称(取第一条记录的 deptId) + String deptName = null; + if (!records.isEmpty() && records.get(0).getDeptId() != null) { + deptName = resolveDeptName(records.get(0).getDeptId()); + } + + WorkloadDetailVO detail = new WorkloadDetailVO(); + detail.setUserId(userId); + detail.setRealName(realName); + detail.setDeptName(deptName); + detail.setDetectionCount(detectionCount); + detail.setCompletedCount(completedCount); + detail.setDelayCount(delayCount); + detail.setDelayRate(delayRate); + detail.setAvgTime(avgTime); + + return detail; + } + + /** + * 构建汇总信息 + */ + private WorkloadSummaryVO buildSummary(List details, + int totalDetectionCount, + BigDecimal sumDelayRate, + Date startDate, + Date endDate, + List allRecords) { + WorkloadSummaryVO summary = new WorkloadSummaryVO(); + summary.setTotalDetectionCount(totalDetectionCount); + summary.setDetails(details); + + // 平均延期率 = 所有人员延期率之和 / 人数 + int personCount = details.size(); + BigDecimal avgDelayRate = personCount > 0 + ? sumDelayRate.divide(BigDecimal.valueOf(personCount), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + summary.setAvgDelayRate(avgDelayRate); + + // 日均每人检测数 = total / 天数 / 人数 + if (startDate != null && endDate != null && personCount > 0) { + long diffMs = endDate.getTime() - startDate.getTime(); + long days = diffMs / (1000 * 60 * 60 * 24); + if (days <= 0) { + days = 1; // 最小为 1 天 + } + BigDecimal avgDaily = BigDecimal.valueOf(totalDetectionCount) + .divide(BigDecimal.valueOf(days), 2, RoundingMode.HALF_UP) + .divide(BigDecimal.valueOf(personCount), 2, RoundingMode.HALF_UP); + summary.setAvgDailyPerPerson(avgDaily); + } else { + summary.setAvgDailyPerPerson(BigDecimal.ZERO); + } + + // 整体平均耗时 + summary.setAvgTime(calculateOverallAvgTime(allRecords)); + + return summary; + } + + /** + * 计算已完成的平均耗时 + */ + private String calculateAvgTime(List records) { + long totalMillis = 0; + int count = 0; + for (Examine e : records) { + if ("1".equals(e.getIsFinished()) + && e.getFinishTime() != null + && e.getReceiveTime() != null) { + totalMillis += e.getFinishTime().getTime() - e.getReceiveTime().getTime(); + count++; + } + } + return formatAvgTime(totalMillis, count); + } + + /** + * 计算整体平均耗时 + */ + private String calculateOverallAvgTime(List allRecords) { + long totalMillis = 0; + int count = 0; + for (Examine e : allRecords) { + if ("1".equals(e.getIsFinished()) + && e.getFinishTime() != null + && e.getReceiveTime() != null) { + totalMillis += e.getFinishTime().getTime() - e.getReceiveTime().getTime(); + count++; + } + } + return formatAvgTime(totalMillis, count); + } + + /** + * 格式化平均耗时 + */ + private String formatAvgTime(long totalMillis, int count) { + if (count == 0) { + return "-"; + } + long avgMillis = totalMillis / count; + long hours = avgMillis / (1000 * 60 * 60); + long minutes = (avgMillis % (1000 * 60 * 60)) / (1000 * 60); + long seconds = (avgMillis % (1000 * 60)) / 1000; + return String.format("%d时%d分%d秒", hours, minutes, seconds); + } + + /** + * 通过 Feign 查询 blade_user 获取用户真实姓名 + */ + private String resolveUserName(String userId) { + try { + R userR = userClient.userInfoById(Long.valueOf(userId)); + if (userR != null && userR.getData() != null) { + String realName = userR.getData().getRealName(); + if (realName != null && !realName.isEmpty()) { + return realName; + } + // 退回到 name + return userR.getData().getName(); + } + } catch (Exception e) { + log.warn("Failed to resolve user name for userId: {}", userId, e); + } + return userId; + } + + /** + * 通过 Feign 查询 blade_dept 获取部门名称 + */ + private String resolveDeptName(Long deptId) { + try { + R deptR = sysClient.getDept(deptId); + if (deptR != null && deptR.getData() != null) { + return deptR.getData().getDeptName(); + } + } catch (Exception e) { + log.warn("Failed to resolve dept name for deptId: {}", deptId, e); + } + return null; + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/util/MonitorUtil.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/util/MonitorUtil.java new file mode 100644 index 0000000..95670d1 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/util/MonitorUtil.java @@ -0,0 +1,132 @@ +package org.springblade.monitor.util; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.Method; + +/** + * 系统监控工具类 + *

+ * 封装 JDK 内置 MXBean 访问,提供 CPU、物理内存等系统级指标的读取方法。 + * 优先使用 {@link com.sun.management.OperatingSystemMXBean} 获取更精确的物理内存数据, + * 若不可用则降级为 {@link java.lang.management.OperatingSystemMXBean} 的基本信息。 + *

+ * + * @author blade + * @since 2026-06-01 + */ +public class MonitorUtil { + + private MonitorUtil() { + // 工具类,禁止实例化 + } + + /** + * 获取系统 CPU 负载平均值(过去 1 分钟) + * + * @return CPU 负载,-1 表示不可用 + */ + public static double getSystemLoadAverage() { + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + return osBean.getSystemLoadAverage(); + } + + /** + * 获取可用 CPU 核心数 + * + * @return 核心数 + */ + public static int getAvailableProcessors() { + return Runtime.getRuntime().availableProcessors(); + } + + /** + * 获取系统 CPU 使用率(百分比 0~100) + *

+ * 优先使用系统负载平均值(Linux/Unix), + * 不可用时(Windows 上 getSystemLoadAverage 始终返回 -1) + * 降级为 {@link com.sun.management.OperatingSystemMXBean#getSystemCpuLoad()}, + * 该方法返回 0.0~1.0 的 CPU 使用率,乘以 100 转换为百分比。 + *

+ * + * @return CPU 使用率 (0~100),-1 表示不可获取 + */ + public static double getCpuUsage() { + double loadAvg = getSystemLoadAverage(); + if (loadAvg >= 0) { + return loadAvg; + } + // Windows 降级:com.sun.management.OperatingSystemMXBean.getSystemCpuLoad() + try { + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + Method method = osBean.getClass().getMethod("getSystemCpuLoad"); + method.setAccessible(true); + Object result = method.invoke(osBean); + if (result instanceof Double) { + double cpuLoad = (Double) result; + if (cpuLoad >= 0) { + return cpuLoad * 100; + } + } + } catch (Exception e) { + // ignore + } + return -1; + } + + /** + * 获取 JVM 内存使用率 (%) + * + * @return 使用率 (0~100),总内存为 0 时返回 0 + */ + public static double getJvmMemoryUsage() { + long total = Runtime.getRuntime().totalMemory(); + long free = Runtime.getRuntime().freeMemory(); + if (total == 0) { + return 0; + } + return (double) (total - free) / total * 100; + } + + /** + * 获取物理内存总大小 (bytes) + *

+ * 通过反射调用 {@link com.sun.management.OperatingSystemMXBean#getTotalPhysicalMemorySize()}, + * 若该接口不可用则返回 -1。 + *

+ * + * @return 物理内存总大小 (bytes),-1 表示不可获取 + */ + public static long getTotalPhysicalMemory() { + try { + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + Method method = osBean.getClass().getMethod("getTotalPhysicalMemorySize"); + method.setAccessible(true); + Object result = method.invoke(osBean); + return result instanceof Long ? (Long) result : -1; + } catch (Exception e) { + return -1; + } + } + + /** + * 获取空闲物理内存大小 (bytes) + *

+ * 通过反射调用 {@link com.sun.management.OperatingSystemMXBean#getFreePhysicalMemorySize()}, + * 若该接口不可用则返回 -1。 + *

+ * + * @return 空闲物理内存大小 (bytes),-1 表示不可获取 + */ + public static long getFreePhysicalMemory() { + try { + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + Method method = osBean.getClass().getMethod("getFreePhysicalMemorySize"); + method.setAccessible(true); + Object result = method.invoke(osBean); + return result instanceof Long ? (Long) result : -1; + } catch (Exception e) { + return -1; + } + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/DiskInfoVO.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/DiskInfoVO.java new file mode 100644 index 0000000..8e7b4d6 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/DiskInfoVO.java @@ -0,0 +1,27 @@ +package org.springblade.monitor.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 磁盘信息 VO + * + * @author blade + * @since 2026-06-01 + */ +@Data +public class DiskInfoVO implements Serializable { + private static final long serialVersionUID = 1L; + + /** 磁盘路径 */ + private String path; + /** 总空间 (bytes) */ + private long totalSpace; + /** 可用空间 (bytes) */ + private long freeSpace; + /** 已用空间 (bytes) */ + private long usedSpace; + /** 使用率 (%) */ + private double usagePercent; +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/MonitorVO.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/MonitorVO.java new file mode 100644 index 0000000..59a7ece --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/MonitorVO.java @@ -0,0 +1,44 @@ +package org.springblade.monitor.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 系统监控指标 VO + * + * @author blade + * @since 2026-06-01 + */ +@Data +public class MonitorVO implements Serializable { + private static final long serialVersionUID = 1L; + + /** CPU 负载 (SystemLoadAverage, -1 表示不可用) */ + private double cpuUsage; + /** CPU 核心数 */ + private int cpuCores; + + /** JVM 总内存 (bytes) */ + private long memoryTotal; + /** JVM 已用内存 (bytes) */ + private long memoryUsed; + /** JVM 空闲内存 (bytes) */ + private long memoryFree; + /** JVM 内存使用率 (%) */ + private double memoryUsage; + + /** 物理内存总大小 (bytes) */ + private long physicalMemoryTotal; + /** 物理内存可用大小 (bytes) */ + private long physicalMemoryFree; + /** 物理内存使用率 (%) */ + private double physicalMemoryUsage; + + /** 磁盘列表 */ + private List diskList; + + /** 网络延迟 (ms) */ + private long networkLatencyMs; +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/ServiceStatusVO.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/ServiceStatusVO.java new file mode 100644 index 0000000..539ef2c --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/ServiceStatusVO.java @@ -0,0 +1,25 @@ +package org.springblade.monitor.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 服务健康状态 VO + * + * @author blade + * @since 2026-06-01 + */ +@Data +public class ServiceStatusVO implements Serializable { + private static final long serialVersionUID = 1L; + + /** 服务名称 */ + private String name; + /** 状态: UP / DOWN */ + private String status; + /** 响应延迟 (ms) */ + private long latencyMs; + /** 错误信息 (状态为 DOWN 时提供) */ + private String errorMessage; +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/TrendPointVO.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/TrendPointVO.java new file mode 100644 index 0000000..a995aa4 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/TrendPointVO.java @@ -0,0 +1,21 @@ +package org.springblade.monitor.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 趋势数据点 VO + * + * @author blade + * @since 2026-06-01 + */ +@Data +public class TrendPointVO implements Serializable { + private static final long serialVersionUID = 1L; + + /** 时间戳 (ms) */ + private long timestamp; + /** 指标值 */ + private double value; +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadDetailVO.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadDetailVO.java new file mode 100644 index 0000000..25097b3 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadDetailVO.java @@ -0,0 +1,41 @@ +package org.springblade.monitor.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 人员工作量明细 VO + * + * @author blade + * @since 2026-06-01 + */ +@Data +public class WorkloadDetailVO implements Serializable { + private static final long serialVersionUID = 1L; + + /** 用户ID (examineBy) */ + private String userId; + + /** 用户真实姓名 */ + private String realName; + + /** 部门名称 */ + private String deptName; + + /** 任务总数 */ + private Integer detectionCount; + + /** 已完成数 */ + private Integer completedCount; + + /** 延期数 */ + private Integer delayCount; + + /** 延期率 (%) */ + private BigDecimal delayRate; + + /** 平均完成耗时 */ + private String avgTime; +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadSummaryVO.java b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadSummaryVO.java new file mode 100644 index 0000000..fba4cf3 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadSummaryVO.java @@ -0,0 +1,33 @@ +package org.springblade.monitor.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * 人员工作量汇总 VO + * + * @author blade + * @since 2026-06-01 + */ +@Data +public class WorkloadSummaryVO implements Serializable { + private static final long serialVersionUID = 1L; + + /** 总检测数 */ + private Integer totalDetectionCount; + + /** 平均延期率 (%) */ + private BigDecimal avgDelayRate; + + /** 日均每人检测数 */ + private BigDecimal avgDailyPerPerson; + + /** 平均完成耗时 */ + private String avgTime; + + /** 人员工作量明细列表 */ + private List details; +}