新增功能:运维监控

feature-wangxilei-dev
wxl 2 weeks ago
parent 0170907f6d
commit 910fe72a1b
  1. 67
      lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricConfig.java
  2. 62
      lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/CustomMetricStat.java
  3. 2
      lab-service/lab-lims/src/main/java/org/springblade/lims/LimsApplication.java
  4. 14
      lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricConfigMapper.java
  5. 14
      lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/CustomMetricStatMapper.java
  6. 14
      lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricConfigService.java
  7. 38
      lab-service/lab-lims/src/main/java/org/springblade/lims/service/ICustomMetricStatService.java
  8. 20
      lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricConfigServiceImpl.java
  9. 412
      lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/CustomMetricStatServiceImpl.java
  10. 232
      lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/CustomMetricConfigController.java
  11. 82
      lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/MonitorController.java
  12. 62
      lab-service/lab-lims/src/main/java/org/springblade/monitor/controller/PersonnelWorkloadController.java
  13. 47
      lab-service/lab-lims/src/main/java/org/springblade/monitor/scheduled/MonitorScheduledTasks.java
  14. 50
      lab-service/lab-lims/src/main/java/org/springblade/monitor/service/MonitorService.java
  15. 28
      lab-service/lab-lims/src/main/java/org/springblade/monitor/service/PersonnelWorkloadService.java
  16. 488
      lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/MonitorServiceImpl.java
  17. 264
      lab-service/lab-lims/src/main/java/org/springblade/monitor/service/impl/PersonnelWorkloadServiceImpl.java
  18. 132
      lab-service/lab-lims/src/main/java/org/springblade/monitor/util/MonitorUtil.java
  19. 27
      lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/DiskInfoVO.java
  20. 44
      lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/MonitorVO.java
  21. 25
      lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/ServiceStatusVO.java
  22. 21
      lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/TrendPointVO.java
  23. 41
      lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadDetailVO.java
  24. 33
      lab-service/lab-lims/src/main/java/org/springblade/monitor/vo/WorkloadSummaryVO.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;
}

@ -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;
}

@ -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 {

@ -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<CustomMetricConfig> {
}

@ -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<CustomMetricStat> {
}

@ -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<CustomMetricConfig> {
}

@ -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<CustomMetricStat> {
/**
* 执行单个配置的计算
*
* @param configId 配置ID
* @return 执行结果
*/
Map<String, Object> executeCalculation(Long configId);
/**
* 执行所有启用的配置
*/
void executeAllConfigs();
/**
* 删除已有计算结果幂等性
*
* @param configId 配置ID
* @param periodStart 周期开始时间
* @param periodEnd 周期结束时间
*/
void deleteExistingResults(Long configId, Date periodStart, Date periodEnd);
}

@ -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<CustomMetricConfigMapper, CustomMetricConfig> implements ICustomMetricConfigService {
}

@ -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<CustomMetricStatMapper, CustomMetricStat> 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<String, Object> 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<CustomMetricStat> results = calculateStats(config, periodStart, periodEnd);
if (!results.isEmpty()) {
saveBatch(results);
}
// 5. 返回结果摘要
Map<String, Object> 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<CustomMetricConfig> 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<CustomMetricStat> 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<CustomMetricStat> calculateUserStats(CustomMetricConfig config, Date periodStart, Date periodEnd) {
String statField = config.getStatField();
if ("reagentUsage".equals(statField)) {
return calculateReagentUsageByUser(config, periodStart, periodEnd);
}
// 查询指定周期内的 Examine 记录
LambdaQueryWrapper<Examine> wrapper = buildExamineQueryWrapper(statField, periodStart, periodEnd);
List<Examine> examines = examineMapper.selectList(wrapper);
// 按 examineBy 分组
Map<String, List<Examine>> byUser = examines.stream()
.filter(e -> e.getExamineBy() != null && !e.getExamineBy().trim().isEmpty())
.collect(Collectors.groupingBy(Examine::getExamineBy));
// 计算每个用户的统计值
Map<String, BigDecimal> userValues = new HashMap<>(byUser.size() * 2);
for (Map.Entry<String, List<Examine>> entry : byUser.entrySet()) {
userValues.put(entry.getKey(), BigDecimal.valueOf(entry.getValue().size()));
}
return buildStatResults(config, periodStart, userValues);
}
/**
* 按试剂使用按用户统计
*/
private List<CustomMetricStat> calculateReagentUsageByUser(CustomMetricConfig config, Date periodStart, Date periodEnd) {
LambdaQueryWrapper<ReagentUseLog> wrapper = new LambdaQueryWrapper<ReagentUseLog>()
.ge(ReagentUseLog::getCreateTime, periodStart)
.le(ReagentUseLog::getCreateTime, periodEnd);
List<ReagentUseLog> logs = reagentUseLogMapper.selectList(wrapper);
// 按创建用户分组并汇总用量
Map<String, BigDecimal> 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<CustomMetricStat> calculateDeptStats(CustomMetricConfig config, Date periodStart, Date periodEnd) {
String statField = config.getStatField();
if ("reagentUsage".equals(statField)) {
return calculateReagentUsageByDept(config, periodStart, periodEnd);
}
// 查询指定周期内的 Examine 记录
LambdaQueryWrapper<Examine> wrapper = buildExamineQueryWrapper(statField, periodStart, periodEnd);
wrapper.isNotNull(Examine::getDeptId);
List<Examine> examines = examineMapper.selectList(wrapper);
// 按 deptId 分组
Map<Long, List<Examine>> byDept = examines.stream()
.filter(e -> e.getDeptId() != null)
.collect(Collectors.groupingBy(Examine::getDeptId));
// 计算每个部门的统计值
Map<String, BigDecimal> deptValues = new HashMap<>(byDept.size() * 2);
for (Map.Entry<Long, List<Examine>> entry : byDept.entrySet()) {
deptValues.put(String.valueOf(entry.getKey()), BigDecimal.valueOf(entry.getValue().size()));
}
return buildDeptStatResults(config, periodStart, deptValues);
}
/**
* 按试剂使用按部门统计
*/
private List<CustomMetricStat> calculateReagentUsageByDept(CustomMetricConfig config, Date periodStart, Date periodEnd) {
LambdaQueryWrapper<ReagentUseLog> wrapper = new LambdaQueryWrapper<ReagentUseLog>()
.ge(ReagentUseLog::getCreateTime, periodStart)
.le(ReagentUseLog::getCreateTime, periodEnd);
List<ReagentUseLog> logs = reagentUseLogMapper.selectList(wrapper);
// 按创建部门分组并汇总用量
Map<String, BigDecimal> 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<Examine> buildExamineQueryWrapper(String statField, Date periodStart, Date periodEnd) {
LambdaQueryWrapper<Examine> wrapper = new LambdaQueryWrapper<Examine>();
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<CustomMetricStat> buildStatResults(CustomMetricConfig config, Date statDate,
Map<String, BigDecimal> userValues) {
List<CustomMetricStat> results = new ArrayList<>();
if ("sum".equals(config.getAggregation())) {
// 每人一条记录
for (Map.Entry<String, BigDecimal> 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<CustomMetricStat> buildDeptStatResults(CustomMetricConfig config, Date statDate,
Map<String, BigDecimal> deptValues) {
List<CustomMetricStat> results = new ArrayList<>();
if ("sum".equals(config.getAggregation())) {
// 每个部门一条记录
for (Map.Entry<String, BigDecimal> 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<User> 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<String> 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);
}
}

@ -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<IPage<CustomMetricConfig>> list(CustomMetricConfig entry, Query query) {
LambdaQueryWrapper<CustomMetricConfig> wrapper = new LambdaQueryWrapper<>();
if (entry.getConfigName() != null) {
wrapper.like(CustomMetricConfig::getConfigName, entry.getConfigName());
}
wrapper.orderByDesc(CustomMetricConfig::getCreateTime);
IPage<CustomMetricConfig> 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)));
}
/**
* 手动触发统计计算
* <p>
* 跳过 DictBizCache 定时开关判断直接执行计算
* 可选传入 configId 执行单个配置为空则执行所有启用的配置
* </p>
*/
@PostMapping("trigger-calculation")
@ApiOperation(value = "手动触发计算", notes = "手动触发自定义指标统计计算,跳过定时开关判断。可选指定配置ID,为空则执行所有启用配置。")
public R<Map<String, Object>> triggerCalculation(
@ApiParam(value = "配置ID,为空则执行所有启用配置")
@RequestParam(required = false) Long configId) {
// Admin-only auth check
// if (!AuthUtil.isAdministrator()) {
// return R.fail("仅管理员可执行此操作");
// }
List<Map<String, Object>> details = new ArrayList<>();
int successCount = 0;
int failCount = 0;
if (configId != null) {
// 执行单个配置
try {
Map<String, Object> detail = customMetricStatService.executeCalculation(configId);
detail.put("status", "success");
successCount = 1;
details.add(detail);
} catch (Exception e) {
failCount = 1;
Map<String, Object> 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<CustomMetricConfig> configs = service.lambdaQuery()
.eq(CustomMetricConfig::getEnabled, 1)
.list();
for (CustomMetricConfig config : configs) {
try {
Map<String, Object> detail = customMetricStatService.executeCalculation(config.getId());
detail.put("status", "success");
successCount++;
details.add(detail);
} catch (Exception e) {
failCount++;
Map<String, Object> 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<String, Object> 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);
}
/**
* 统计结果分页列表
* <p>
* 分页查询自定义指标统计结果附带配置名称
* </p>
*/
@GetMapping("metric-stat/list")
@ApiOperationSupport(order = 5)
@ApiOperation(value = "统计结果分页列表", notes = "分页查询自定义指标统计结果")
public R<Map<String, Object>> 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<CustomMetricStat> 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<CustomMetricStat> page = customMetricStatService.page(Condition.getPage(query), wrapper);
// Enrich records with config name
List<Map<String, Object>> records = new ArrayList<>(page.getRecords().size());
for (CustomMetricStat stat : page.getRecords()) {
Map<String, Object> 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<String, Object> 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);
}
}

@ -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;
/**
* 系统监控指标
* <p>
* 获取系统 CPUJVM 内存物理内存磁盘使用率网络延迟等指标
* 每次调用会自动更新趋势数据缓冲区
* </p>
*/
@GetMapping("/system")
@ApiOperation(value = "系统监控指标", notes = "获取系统 CPU、内存、磁盘等指标,同时更新趋势缓冲区")
public R<MonitorVO> system() {
MonitorVO metrics = monitorService.getSystemMetrics();
return R.data(metrics);
}
/**
* 服务健康状态列表
* <p>
* 检查 Java 应用MySQLRedis 等服务健康状态
* 每次调用前会自动采集系统指标以更新趋势缓冲区
* </p>
*/
@GetMapping("/services")
@ApiOperation(value = "服务健康状态", notes = "检查 Java、MySQL、Redis 等服务健康状态")
public R<List<ServiceStatusVO>> services() {
// 采集系统指标以更新趋势缓冲区
monitorService.getSystemMetrics();
List<ServiceStatusVO> statusList = monitorService.getServiceStatus();
return R.data(statusList);
}
/**
* 趋势数据
* <p>
* 获取指定指标的最近趋势数据
* type 可选值: cpu, memory, disk, network
* 每次调用前会自动采集系统指标以更新趋势缓冲区
* </p>
*/
@GetMapping("/trend")
@ApiOperation(value = "趋势数据", notes = "获取指定指标的最近趋势数据,type 可选值: cpu / memory / disk / network")
public R<List<TrendPointVO>> trend(
@ApiParam(value = "指标类型 (cpu / memory / disk / network)", required = true)
@RequestParam String type,
@ApiParam(value = "返回数据点数,默认 60")
@RequestParam(defaultValue = "60") int points) {
// 采集系统指标以更新趋势缓冲区
monitorService.getSystemMetrics();
List<TrendPointVO> trend = monitorService.getTrend(type, points);
return R.data(trend);
}
}

@ -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<WorkloadSummaryVO> 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);
}
}

@ -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);
}
}
}

@ -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;
/**
* 系统监控服务接口
* <p>
* 提供系统指标采集服务健康检查网络延迟测量趋势数据查询功能
* 所有数据均通过 JDK 内置 API 采集无需第三方依赖
* </p>
*
* @author blade
* @since 2026-06-01
*/
public interface MonitorService {
/**
* 获取系统监控指标CPU内存磁盘
*
* @return MonitorVO 包含完整的系统资源指标
*/
MonitorVO getSystemMetrics();
/**
* 检查各服务健康状态
*
* @return 服务状态列表Java 应用 / MySQL / Redis
*/
List<ServiceStatusVO> getServiceStatus();
/**
* 测量到目标主机的网络延迟
*
* @return 延迟 (ms)失败时返回 -1
*/
Long getNetworkLatency();
/**
* 获取指定指标的最近趋势数据
*
* @param type 指标类型 (cpu / memory / disk / network)
* @param points 返回的数据点数
* @return 趋势数据点列表
*/
List<TrendPointVO> getTrend(String type, int points);
}

@ -0,0 +1,28 @@
package org.springblade.monitor.service;
import org.springblade.monitor.vo.WorkloadSummaryVO;
import java.util.Date;
/**
* 人员工作量统计服务接口
* <p>
* 基于 Examine 检验记录按人员维度统计工作量完成率延期率平均耗时等指标
* 跨数据源查询Examine (sxm_system) + blade_user / blade_dept (sxmlims)
* </p>
*
* @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);
}

@ -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;
/**
* 系统监控服务实现
* <p>
* 使用 JDK 内置 API 采集系统指标通过 HttpURLConnection / DataSource / RedisTemplate
* 进行健康检查通过 Socket 测量网络延迟使用 ConcurrentLinkedQueue 维护趋势数据缓冲区
* </p>
*
* @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<String, Object> redisTemplate;
public MonitorServiceImpl(DataSource dataSource, RedisTemplate<String, Object> 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<Map<String, Object>> 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<DiskInfoVO> 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<ServiceStatusVO> getServiceStatus() {
List<ServiceStatusVO> 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<TrendPointVO> getTrend(String type, int points) {
List<TrendPointVO> 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<String, Object> snapshot = (Map<String, Object>) 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 应用健康状态
* <p>
* 若配置了 monitor.health.java.urls逗号分隔对每个 URL 进行 HTTP 健康检查
* 否则通过系统调用jps / wmic检测本地运行的 Java 进程
* </p>
*/
private List<ServiceStatusVO> checkJavaServices() {
String[] urls = parseJavaHealthUrls();
if (urls.length > 0) {
return checkRemoteJavaServices(urls);
}
return detectLocalJavaProcesses();
}
/**
* 对配置的 URL 列表进行 HTTP 健康检查
*/
private List<ServiceStatusVO> checkRemoteJavaServices(String[] urls) {
List<ServiceStatusVO> results = new ArrayList<>();
for (String url : urls) {
String trimmed = url.trim();
if (!trimmed.isEmpty()) {
results.add(checkSingleJavaService(trimmed, extractAppName(trimmed)));
}
}
return results;
}
/**
* 通过系统调用检测本地 Java 进程
* <p>
* 优先使用 JDK 自带的 jps -l 命令显示主类名
* 若不可用则降级为 wmic 命令
* </p>
*/
private List<ServiceStatusVO> detectLocalJavaProcesses() {
List<ServiceStatusVO> 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<ServiceStatusVO> 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<ServiceStatusVO> 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<String, Object> 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();
}
}
}

@ -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;
/**
* 人员工作量统计服务实现
* <p>
* 基于 Examine 检验记录按人员维度统计工作量
* 跨数据源通过 Feign 客户端查询 blade_user / blade_dept
* </p>
*
* @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<Examine> examineList = queryExamineRecords(startDate, endDate, deptId);
// 2. 按 examineBy 分组
Map<String, List<Examine>> groupByUser = examineList.stream()
.filter(e -> e.getExamineBy() != null && !e.getExamineBy().isEmpty())
.collect(Collectors.groupingBy(Examine::getExamineBy));
// 3. 构建明细
List<WorkloadDetailVO> details = new ArrayList<>();
int totalDetectionCount = 0;
BigDecimal sumDelayRate = BigDecimal.ZERO;
for (Map.Entry<String, List<Examine>> 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<Examine> queryExamineRecords(Date startDate, Date endDate, Long deptId) {
LambdaQueryWrapper<Examine> 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<Examine> 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<WorkloadDetailVO> details,
int totalDetectionCount,
BigDecimal sumDelayRate,
Date startDate,
Date endDate,
List<Examine> 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<Examine> 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<Examine> 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<User> 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<Dept> 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;
}
}

@ -0,0 +1,132 @@
package org.springblade.monitor.util;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.reflect.Method;
/**
* 系统监控工具类
* <p>
* 封装 JDK 内置 MXBean 访问提供 CPU物理内存等系统级指标的读取方法
* 优先使用 {@link com.sun.management.OperatingSystemMXBean} 获取更精确的物理内存数据
* 若不可用则降级为 {@link java.lang.management.OperatingSystemMXBean} 的基本信息
* </p>
*
* @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
* <p>
* 优先使用系统负载平均值Linux/Unix
* 不可用时Windows getSystemLoadAverage 始终返回 -1
* 降级为 {@link com.sun.management.OperatingSystemMXBean#getSystemCpuLoad()}
* 该方法返回 0.0~1.0 CPU 使用率乘以 100 转换为百分比
* </p>
*
* @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)
* <p>
* 通过反射调用 {@link com.sun.management.OperatingSystemMXBean#getTotalPhysicalMemorySize()}
* 若该接口不可用则返回 -1
* </p>
*
* @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)
* <p>
* 通过反射调用 {@link com.sun.management.OperatingSystemMXBean#getFreePhysicalMemorySize()}
* 若该接口不可用则返回 -1
* </p>
*
* @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;
}
}
}

@ -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;
}

@ -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<DiskInfoVO> diskList;
/** 网络延迟 (ms) */
private long networkLatencyMs;
}

@ -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;
}

@ -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;
}

@ -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;
}

@ -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<WorkloadDetailVO> details;
}
Loading…
Cancel
Save