parent
0170907f6d
commit
910fe72a1b
24 changed files with 2218 additions and 1 deletions
@ -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; |
||||||
|
} |
||||||
@ -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> |
||||||
|
* 获取系统 CPU、JVM 内存、物理内存、磁盘使用率、网络延迟等指标。 |
||||||
|
* 每次调用会自动更新趋势数据缓冲区。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@GetMapping("/system") |
||||||
|
@ApiOperation(value = "系统监控指标", notes = "获取系统 CPU、内存、磁盘等指标,同时更新趋势缓冲区") |
||||||
|
public R<MonitorVO> system() { |
||||||
|
MonitorVO metrics = monitorService.getSystemMetrics(); |
||||||
|
return R.data(metrics); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 服务健康状态列表 |
||||||
|
* <p> |
||||||
|
* 检查 Java 应用、MySQL、Redis 等服务健康状态。 |
||||||
|
* 每次调用前会自动采集系统指标以更新趋势缓冲区。 |
||||||
|
* </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…
Reference in new issue