feat: add equipment health and failure tracking
This commit is contained in:
parent
9e89932600
commit
fd3ffc2b79
|
|
@ -0,0 +1,111 @@
|
||||||
|
package com.ether.pms.mdm.controller;
|
||||||
|
|
||||||
|
import com.ether.pms.common.ApiResponse;
|
||||||
|
import com.ether.pms.mdm.entity.EquipmentFailureHistory;
|
||||||
|
import com.ether.pms.mdm.entity.EquipmentHealthScore;
|
||||||
|
import com.ether.pms.mdm.service.EquipmentHealthService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/ops")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EquipmentHealthController {
|
||||||
|
|
||||||
|
private final EquipmentHealthService equipmentHealthService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备健康度
|
||||||
|
*/
|
||||||
|
@GetMapping("/equipment-health/{equipmentId}")
|
||||||
|
public ApiResponse<EquipmentHealthScore> getEquipmentHealth(@PathVariable UUID equipmentId) {
|
||||||
|
return ApiResponse.success(equipmentHealthService.getLatestHealthScore(equipmentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备健康度历史
|
||||||
|
*/
|
||||||
|
@GetMapping("/equipment-health/{equipmentId}/history")
|
||||||
|
public ApiResponse<List<EquipmentHealthScore>> getHealthHistory(@PathVariable UUID equipmentId) {
|
||||||
|
return ApiResponse.success(equipmentHealthService.getHealthHistory(equipmentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算设备健康度
|
||||||
|
*/
|
||||||
|
@PostMapping("/equipment-health/calculate")
|
||||||
|
public ApiResponse<EquipmentHealthScore> calculateHealthScore(@RequestBody CalculateHealthRequest request) {
|
||||||
|
return ApiResponse.success(equipmentHealthService.calculateHealthScore(request.getEquipmentId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录故障
|
||||||
|
*/
|
||||||
|
@PostMapping("/equipment-failure-history")
|
||||||
|
public ApiResponse<EquipmentFailureHistory> recordFailure(@Valid @RequestBody EquipmentFailureHistory failure) {
|
||||||
|
return ApiResponse.success(equipmentHealthService.recordFailure(failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备故障历史
|
||||||
|
*/
|
||||||
|
@GetMapping("/equipment-failure-history/{equipmentId}")
|
||||||
|
public ApiResponse<List<EquipmentFailureHistory>> getFailureHistory(@PathVariable UUID equipmentId) {
|
||||||
|
return ApiResponse.success(equipmentHealthService.getFailureHistory(equipmentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备MTBF(平均故障间隔时间)
|
||||||
|
*/
|
||||||
|
@GetMapping("/equipment-mtbf/{equipmentId}")
|
||||||
|
public ApiResponse<MTBFResponse> getEquipmentMTBF(
|
||||||
|
@PathVariable UUID equipmentId,
|
||||||
|
@RequestParam(required = false, defaultValue = "30") Integer days) {
|
||||||
|
BigDecimal mtbf = equipmentHealthService.calculateMTBF(equipmentId, days);
|
||||||
|
MTBFResponse response = new MTBFResponse();
|
||||||
|
response.setEquipmentId(equipmentId);
|
||||||
|
response.setDays(days);
|
||||||
|
response.setMtbfHours(mtbf);
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备MTTR(平均修复时间)
|
||||||
|
*/
|
||||||
|
@GetMapping("/equipment-mttr/{equipmentId}")
|
||||||
|
public ApiResponse<MTTRResponse> getEquipmentMTTR(
|
||||||
|
@PathVariable UUID equipmentId,
|
||||||
|
@RequestParam(required = false, defaultValue = "30") Integer days) {
|
||||||
|
BigDecimal mttr = equipmentHealthService.calculateMTTR(equipmentId, days);
|
||||||
|
MTTRResponse response = new MTTRResponse();
|
||||||
|
response.setEquipmentId(equipmentId);
|
||||||
|
response.setDays(days);
|
||||||
|
response.setMttrHours(mttr);
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class CalculateHealthRequest {
|
||||||
|
private UUID equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class MTBFResponse {
|
||||||
|
private UUID equipmentId;
|
||||||
|
private Integer days;
|
||||||
|
private BigDecimal mtbfHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class MTTRResponse {
|
||||||
|
private UUID equipmentId;
|
||||||
|
private Integer days;
|
||||||
|
private BigDecimal mttrHours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
package com.ether.pms.mdm.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "ops_equipment_failure_history", indexes = {
|
||||||
|
@Index(name = "idx_failure_equipment", columnList = "equipment_id"),
|
||||||
|
@Index(name = "idx_failure_time", columnList = "failure_time"),
|
||||||
|
@Index(name = "idx_failure_project", columnList = "project_id")
|
||||||
|
})
|
||||||
|
@Data
|
||||||
|
public class EquipmentFailureHistory {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "project_id", nullable = false)
|
||||||
|
private UUID projectId;
|
||||||
|
|
||||||
|
@Column(name = "equipment_id", nullable = false)
|
||||||
|
private UUID equipmentId;
|
||||||
|
|
||||||
|
@Column(name = "equipment_code", length = 50)
|
||||||
|
private String equipmentCode;
|
||||||
|
|
||||||
|
@Column(name = "equipment_name", length = 100)
|
||||||
|
private String equipmentName;
|
||||||
|
|
||||||
|
@Column(name = "failure_time", nullable = false)
|
||||||
|
private LocalDateTime failureTime;
|
||||||
|
|
||||||
|
@Column(name = "failure_type", length = 50)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private FailureType failureType;
|
||||||
|
|
||||||
|
@Column(name = "failure_level", length = 20)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private FailureLevel failureLevel;
|
||||||
|
|
||||||
|
@Column(name = "failure_reason", columnDefinition = "TEXT")
|
||||||
|
private String failureReason;
|
||||||
|
|
||||||
|
@Column(name = "failure_description", columnDefinition = "TEXT")
|
||||||
|
private String failureDescription;
|
||||||
|
|
||||||
|
@Column(name = "repair_start_time")
|
||||||
|
private LocalDateTime repairStartTime;
|
||||||
|
|
||||||
|
@Column(name = "repair_end_time")
|
||||||
|
private LocalDateTime repairEndTime;
|
||||||
|
|
||||||
|
@Column(name = "repair_duration_hours", precision = 10, scale = 2)
|
||||||
|
private Double repairDurationHours;
|
||||||
|
|
||||||
|
@Column(name = "repair_person", length = 100)
|
||||||
|
private String repairPerson;
|
||||||
|
|
||||||
|
@Column(name = "repair_result", length = 20)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private RepairResult repairResult;
|
||||||
|
|
||||||
|
@Column(name = "downtime_hours", precision = 10, scale = 2)
|
||||||
|
private Double downtimeHours;
|
||||||
|
|
||||||
|
@Column(name = "maintenance_cost", precision = 12, scale = 2)
|
||||||
|
private java.math.BigDecimal maintenanceCost;
|
||||||
|
|
||||||
|
@Column(name = "is_scheduled", nullable = false)
|
||||||
|
private Boolean isScheduled = false;
|
||||||
|
|
||||||
|
@Column(name = "related_task_id")
|
||||||
|
private UUID relatedTaskId;
|
||||||
|
|
||||||
|
@Column(name = "remarks", columnDefinition = "TEXT")
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Column(name = "created_by")
|
||||||
|
private UUID createdBy;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
public void prePersist() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
if (isScheduled == null) {
|
||||||
|
isScheduled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate() {
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FailureType {
|
||||||
|
SUDDEN("突发故障"),
|
||||||
|
SCHEDULED("计划停机"),
|
||||||
|
WARNING("预警"),
|
||||||
|
OTHER("其他");
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
FailureType(String desc) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FailureLevel {
|
||||||
|
LOW("轻微"),
|
||||||
|
MEDIUM("一般"),
|
||||||
|
HIGH("严重"),
|
||||||
|
CRITICAL("危急");
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
FailureLevel(String desc) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RepairResult {
|
||||||
|
FIXED("已修复"),
|
||||||
|
PARTIAL_FIXED("部分修复"),
|
||||||
|
REPLACED("更换新设备"),
|
||||||
|
PENDING("待处理");
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
RepairResult(String desc) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
package com.ether.pms.mdm.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "ops_equipment_health_score", indexes = {
|
||||||
|
@Index(name = "idx_health_equipment", columnList = "equipment_id"),
|
||||||
|
@Index(name = "idx_health_calc_time", columnList = "calculated_at"),
|
||||||
|
@Index(name = "idx_health_project", columnList = "project_id")
|
||||||
|
})
|
||||||
|
@Data
|
||||||
|
public class EquipmentHealthScore {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "project_id", nullable = false)
|
||||||
|
private UUID projectId;
|
||||||
|
|
||||||
|
@Column(name = "equipment_id", nullable = false)
|
||||||
|
private UUID equipmentId;
|
||||||
|
|
||||||
|
@Column(name = "equipment_code", length = 50)
|
||||||
|
private String equipmentCode;
|
||||||
|
|
||||||
|
@Column(name = "equipment_name", length = 100)
|
||||||
|
private String equipmentName;
|
||||||
|
|
||||||
|
@Column(name = "health_score", nullable = false, precision = 5, scale = 2)
|
||||||
|
private BigDecimal healthScore;
|
||||||
|
|
||||||
|
@Column(name = "failure_deduction", precision = 5, scale = 2)
|
||||||
|
private BigDecimal failureDeduction;
|
||||||
|
|
||||||
|
@Column(name = "maintenance_deduction", precision = 5, scale = 2)
|
||||||
|
private BigDecimal maintenanceDeduction;
|
||||||
|
|
||||||
|
@Column(name = "age_deduction", precision = 5, scale = 2)
|
||||||
|
private BigDecimal ageDeduction;
|
||||||
|
|
||||||
|
@Column(name = "failure_count_30d")
|
||||||
|
private Integer failureCount30d;
|
||||||
|
|
||||||
|
@Column(name = "maintenance_completion_rate", precision = 5, scale = 4)
|
||||||
|
private BigDecimal maintenanceCompletionRate;
|
||||||
|
|
||||||
|
@Column(name = "equipment_age_years", precision = 10, scale = 2)
|
||||||
|
private BigDecimal equipmentAgeYears;
|
||||||
|
|
||||||
|
@Column(name = "operation_hours_30d", precision = 12, scale = 2)
|
||||||
|
private BigDecimal operationHours30d;
|
||||||
|
|
||||||
|
@Column(name = "mtbf_hours", precision = 12, scale = 2)
|
||||||
|
private BigDecimal mtbfHours;
|
||||||
|
|
||||||
|
@Column(name = "mttr_hours", precision = 12, scale = 2)
|
||||||
|
private BigDecimal mttrHours;
|
||||||
|
|
||||||
|
@Column(name = "health_level", length = 20)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private HealthLevel healthLevel;
|
||||||
|
|
||||||
|
@Column(name = "calculated_at", nullable = false)
|
||||||
|
private LocalDateTime calculatedAt;
|
||||||
|
|
||||||
|
@Column(name = "calculation_period_days")
|
||||||
|
private Integer calculationPeriodDays;
|
||||||
|
|
||||||
|
@Column(name = "remarks", columnDefinition = "TEXT")
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
public void prePersist() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
if (calculatedAt == null) {
|
||||||
|
calculatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate() {
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HealthLevel {
|
||||||
|
EXCELLENT("优秀", 90, 100),
|
||||||
|
GOOD("良好", 75, 90),
|
||||||
|
FAIR("一般", 60, 75),
|
||||||
|
POOR("较差", 40, 60),
|
||||||
|
CRITICAL("危急", 0, 40);
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
private final int minScore;
|
||||||
|
private final int maxScore;
|
||||||
|
|
||||||
|
HealthLevel(String desc, int minScore, int maxScore) {
|
||||||
|
this.desc = desc;
|
||||||
|
this.minScore = minScore;
|
||||||
|
this.maxScore = maxScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinScore() {
|
||||||
|
return minScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxScore() {
|
||||||
|
return maxScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HealthLevel fromScore(BigDecimal score) {
|
||||||
|
if (score == null) {
|
||||||
|
return CRITICAL;
|
||||||
|
}
|
||||||
|
double s = score.doubleValue();
|
||||||
|
for (HealthLevel level : values()) {
|
||||||
|
if (s >= level.minScore && s < level.maxScore) {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CRITICAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.ether.pms.mdm.repository;
|
||||||
|
|
||||||
|
import com.ether.pms.mdm.entity.EquipmentFailureHistory;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface EquipmentFailureHistoryRepository extends JpaRepository<EquipmentFailureHistory, UUID> {
|
||||||
|
|
||||||
|
List<EquipmentFailureHistory> findByEquipmentIdOrderByFailureTimeDesc(UUID equipmentId);
|
||||||
|
|
||||||
|
List<EquipmentFailureHistory> findByProjectIdAndFailureTimeBetweenOrderByFailureTimeDesc(
|
||||||
|
UUID projectId, LocalDateTime startTime, LocalDateTime endTime);
|
||||||
|
|
||||||
|
@Query("SELECT f FROM EquipmentFailureHistory f WHERE f.equipmentId = :equipmentId AND f.failureTime >= :since ORDER BY f.failureTime DESC")
|
||||||
|
List<EquipmentFailureHistory> findByEquipmentIdSince(@Param("equipmentId") UUID equipmentId, @Param("since") LocalDateTime since);
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(f) FROM EquipmentFailureHistory f WHERE f.equipmentId = :equipmentId AND f.failureTime >= :since")
|
||||||
|
long countByEquipmentIdSince(@Param("equipmentId") UUID equipmentId, @Param("since") LocalDateTime since);
|
||||||
|
|
||||||
|
@Query("SELECT f FROM EquipmentFailureHistory f WHERE f.equipmentId = :equipmentId AND f.repairEndTime IS NOT NULL AND f.failureTime >= :since ORDER BY f.failureTime DESC")
|
||||||
|
List<EquipmentFailureHistory> findRepairedFailuresByEquipmentIdSince(@Param("equipmentId") UUID equipmentId, @Param("since") LocalDateTime since);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.ether.pms.mdm.repository;
|
||||||
|
|
||||||
|
import com.ether.pms.mdm.entity.EquipmentHealthScore;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface EquipmentHealthScoreRepository extends JpaRepository<EquipmentHealthScore, UUID> {
|
||||||
|
|
||||||
|
List<EquipmentHealthScore> findByEquipmentIdOrderByCalculatedAtDesc(UUID equipmentId);
|
||||||
|
|
||||||
|
@Query("SELECT h FROM EquipmentHealthScore h WHERE h.equipmentId = :equipmentId ORDER BY h.calculatedAt DESC LIMIT 1")
|
||||||
|
Optional<EquipmentHealthScore> findLatestByEquipmentId(@Param("equipmentId") UUID equipmentId);
|
||||||
|
|
||||||
|
@Query("SELECT h FROM EquipmentHealthScore h WHERE h.equipmentId = :equipmentId AND h.calculatedAt >= :since ORDER BY h.calculatedAt DESC")
|
||||||
|
List<EquipmentHealthScore> findByEquipmentIdSince(@Param("equipmentId") UUID equipmentId, @Param("since") LocalDateTime since);
|
||||||
|
|
||||||
|
@Query("SELECT h FROM EquipmentHealthScore h WHERE h.projectId = :projectId ORDER BY h.calculatedAt DESC")
|
||||||
|
List<EquipmentHealthScore> findByProjectIdOrderByCalculatedAtDesc(@Param("projectId") UUID projectId);
|
||||||
|
|
||||||
|
@Query("SELECT h FROM EquipmentHealthScore h WHERE h.projectId = :projectId AND h.healthLevel IN :levels ORDER BY h.healthScore ASC")
|
||||||
|
List<EquipmentHealthScore> findByProjectIdAndHealthLevelIn(@Param("projectId") UUID projectId, @Param("levels") List<EquipmentHealthScore.HealthLevel> levels);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.ether.pms.mdm.service;
|
||||||
|
|
||||||
|
import com.ether.pms.mdm.entity.EquipmentFailureHistory;
|
||||||
|
import com.ether.pms.mdm.entity.EquipmentHealthScore;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface EquipmentHealthService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算设备健康度
|
||||||
|
* 基础分 = 100
|
||||||
|
* 故障率扣分 = 故障次数 × 5(近30天每故障扣5分)
|
||||||
|
* 维保扣分 = (1 - 维保完成率) × 20
|
||||||
|
* 年龄扣分 = 投入使用年限 × 2(最高扣10分)
|
||||||
|
* 健康度 = 基础分 - 故障率扣分 - 维保扣分 - 年龄扣分
|
||||||
|
*/
|
||||||
|
EquipmentHealthScore calculateHealthScore(UUID equipmentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算平均故障间隔时间 (MTBF)
|
||||||
|
* MTBF = 运行时间 / 故障次数
|
||||||
|
*/
|
||||||
|
BigDecimal calculateMTBF(UUID equipmentId, Integer days);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算平均修复时间 (MTTR)
|
||||||
|
* MTTR = 总修复时间 / 修复次数
|
||||||
|
*/
|
||||||
|
BigDecimal calculateMTTR(UUID equipmentId, Integer days);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录故障
|
||||||
|
*/
|
||||||
|
EquipmentFailureHistory recordFailure(EquipmentFailureHistory failure);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备健康度历史
|
||||||
|
*/
|
||||||
|
List<EquipmentHealthScore> getHealthHistory(UUID equipmentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备最新健康度
|
||||||
|
*/
|
||||||
|
EquipmentHealthScore getLatestHealthScore(UUID equipmentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备的故障历史
|
||||||
|
*/
|
||||||
|
List<EquipmentFailureHistory> getFailureHistory(UUID equipmentId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
package com.ether.pms.mdm.service.impl;
|
||||||
|
|
||||||
|
import com.ether.pms.common.BusinessException;
|
||||||
|
import com.ether.pms.mdm.entity.EquipmentFailureHistory;
|
||||||
|
import com.ether.pms.mdm.entity.EquipmentHealthScore;
|
||||||
|
import com.ether.pms.mdm.entity.MaintenanceTask;
|
||||||
|
import com.ether.pms.mdm.entity.SpaceNode;
|
||||||
|
import com.ether.pms.mdm.repository.EquipmentFailureHistoryRepository;
|
||||||
|
import com.ether.pms.mdm.repository.EquipmentHealthScoreRepository;
|
||||||
|
import com.ether.pms.mdm.repository.MaintenanceTaskRepository;
|
||||||
|
import com.ether.pms.mdm.repository.SpaceNodeRepository;
|
||||||
|
import com.ether.pms.mdm.service.EquipmentHealthService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.Period;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EquipmentHealthServiceImpl implements EquipmentHealthService {
|
||||||
|
|
||||||
|
private final EquipmentFailureHistoryRepository failureHistoryRepository;
|
||||||
|
private final EquipmentHealthScoreRepository healthScoreRepository;
|
||||||
|
private final SpaceNodeRepository spaceNodeRepository;
|
||||||
|
private final MaintenanceTaskRepository maintenanceTaskRepository;
|
||||||
|
|
||||||
|
private static final BigDecimal BASE_SCORE = new BigDecimal("100");
|
||||||
|
private static final BigDecimal FAILURE_DEDUCTION_PER_COUNT = new BigDecimal("5");
|
||||||
|
private static final BigDecimal MAINTENANCE_DEDUCTION_FACTOR = new BigDecimal("20");
|
||||||
|
private static final BigDecimal AGE_DEDUCTION_PER_YEAR = new BigDecimal("2");
|
||||||
|
private static final BigDecimal MAX_AGE_DEDUCTION = new BigDecimal("10");
|
||||||
|
private static final int DEFAULT_DAYS = 30;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public EquipmentHealthScore calculateHealthScore(UUID equipmentId) {
|
||||||
|
SpaceNode equipment = spaceNodeRepository.findByIdAndIsDeletedFalse(equipmentId)
|
||||||
|
.orElseThrow(() -> new BusinessException(6001, "设备不存在"));
|
||||||
|
|
||||||
|
if (!Boolean.TRUE.equals(equipment.getIsEquipment())) {
|
||||||
|
throw new BusinessException(6002, "该空间节点不是设备");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取近30天故障次数
|
||||||
|
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
|
||||||
|
long failureCount30d = failureHistoryRepository.countByEquipmentIdSince(equipmentId, thirtyDaysAgo);
|
||||||
|
|
||||||
|
// 计算故障率扣分
|
||||||
|
BigDecimal failureDeduction = FAILURE_DEDUCTION_PER_COUNT.multiply(BigDecimal.valueOf(failureCount30d));
|
||||||
|
|
||||||
|
// 计算维保完成率
|
||||||
|
List<MaintenanceTask> tasks = maintenanceTaskRepository.findByEquipmentIdAndStatusNot(equipmentId, MaintenanceTask.Status.CANCELLED);
|
||||||
|
long totalTasks = tasks.size();
|
||||||
|
long completedTasks = tasks.stream()
|
||||||
|
.filter(t -> t.getStatus() == MaintenanceTask.Status.COMPLETED)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
BigDecimal maintenanceCompletionRate = BigDecimal.ONE;
|
||||||
|
if (totalTasks > 0) {
|
||||||
|
maintenanceCompletionRate = BigDecimal.valueOf(completedTasks)
|
||||||
|
.divide(BigDecimal.valueOf(totalTasks), 4, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
BigDecimal maintenanceDeduction = BigDecimal.ONE.subtract(maintenanceCompletionRate)
|
||||||
|
.multiply(MAINTENANCE_DEDUCTION_FACTOR);
|
||||||
|
|
||||||
|
// 计算年龄扣分
|
||||||
|
BigDecimal equipmentAgeYears = calculateEquipmentAge(equipment);
|
||||||
|
BigDecimal ageDeduction = equipmentAgeYears.multiply(AGE_DEDUCTION_PER_YEAR);
|
||||||
|
if (ageDeduction.compareTo(MAX_AGE_DEDUCTION) > 0) {
|
||||||
|
ageDeduction = MAX_AGE_DEDUCTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算健康度
|
||||||
|
BigDecimal healthScore = BASE_SCORE
|
||||||
|
.subtract(failureDeduction)
|
||||||
|
.subtract(maintenanceDeduction)
|
||||||
|
.subtract(ageDeduction);
|
||||||
|
|
||||||
|
// 确保健康度不低于0
|
||||||
|
if (healthScore.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
healthScore = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算MTBF和MTTR
|
||||||
|
BigDecimal mtbf = calculateMTBF(equipmentId, DEFAULT_DAYS);
|
||||||
|
BigDecimal mttr = calculateMTTR(equipmentId, DEFAULT_DAYS);
|
||||||
|
|
||||||
|
// 创建健康度记录
|
||||||
|
EquipmentHealthScore health = new EquipmentHealthScore();
|
||||||
|
health.setProjectId(equipment.getProjectId());
|
||||||
|
health.setEquipmentId(equipmentId);
|
||||||
|
health.setEquipmentCode(equipment.getCode());
|
||||||
|
health.setEquipmentName(equipment.getName());
|
||||||
|
health.setHealthScore(healthScore.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
health.setFailureDeduction(failureDeduction.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
health.setMaintenanceDeduction(maintenanceDeduction.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
health.setAgeDeduction(ageDeduction.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
health.setFailureCount30d((int) failureCount30d);
|
||||||
|
health.setMaintenanceCompletionRate(maintenanceCompletionRate.setScale(4, RoundingMode.HALF_UP));
|
||||||
|
health.setEquipmentAgeYears(equipmentAgeYears.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
health.setMtbfHours(mtbf != null ? mtbf : BigDecimal.ZERO);
|
||||||
|
health.setMttrHours(mttr != null ? mttr : BigDecimal.ZERO);
|
||||||
|
health.setHealthLevel(EquipmentHealthScore.HealthLevel.fromScore(healthScore));
|
||||||
|
health.setCalculatedAt(LocalDateTime.now());
|
||||||
|
health.setCalculationPeriodDays(DEFAULT_DAYS);
|
||||||
|
|
||||||
|
return healthScoreRepository.save(health);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal calculateMTBF(UUID equipmentId, Integer days) {
|
||||||
|
if (days == null || days <= 0) {
|
||||||
|
days = DEFAULT_DAYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime since = LocalDateTime.now().minusDays(days);
|
||||||
|
List<EquipmentFailureHistory> failures = failureHistoryRepository.findByEquipmentIdSince(equipmentId, since);
|
||||||
|
|
||||||
|
if (failures.isEmpty()) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算运行时间(从第一个故障到最后一个故障,或者到现在)
|
||||||
|
LocalDateTime firstFailure = failures.get(failures.size() - 1).getFailureTime();
|
||||||
|
LocalDateTime lastFailure = failures.get(0).getFailureTime();
|
||||||
|
|
||||||
|
// 如果只有一个故障,MTBF = 总时间 / 故障次数 = 0(因为还在运行)
|
||||||
|
// 这里我们用总时间跨度 / 故障次数
|
||||||
|
long totalHours = ChronoUnit.HOURS.between(firstFailure, lastFailure);
|
||||||
|
if (totalHours <= 0) {
|
||||||
|
totalHours = days * 24L; // 使用默认期间
|
||||||
|
}
|
||||||
|
|
||||||
|
// MTBF = 运行时间 / 故障次数
|
||||||
|
BigDecimal mtbf = BigDecimal.valueOf(totalHours)
|
||||||
|
.divide(BigDecimal.valueOf(failures.size()), 2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
return mtbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal calculateMTTR(UUID equipmentId, Integer days) {
|
||||||
|
if (days == null || days <= 0) {
|
||||||
|
days = DEFAULT_DAYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime since = LocalDateTime.now().minusDays(days);
|
||||||
|
List<EquipmentFailureHistory> repairedFailures = failureHistoryRepository
|
||||||
|
.findRepairedFailuresByEquipmentIdSince(equipmentId, since);
|
||||||
|
|
||||||
|
if (repairedFailures.isEmpty()) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总修复时间
|
||||||
|
double totalRepairHours = repairedFailures.stream()
|
||||||
|
.filter(f -> f.getRepairDurationHours() != null)
|
||||||
|
.mapToDouble(EquipmentFailureHistory::getRepairDurationHours)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
// MTTR = 总修复时间 / 修复次数
|
||||||
|
BigDecimal mttr = BigDecimal.valueOf(totalRepairHours)
|
||||||
|
.divide(BigDecimal.valueOf(repairedFailures.size()), 2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
return mttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public EquipmentFailureHistory recordFailure(EquipmentFailureHistory failure) {
|
||||||
|
// 校验设备存在
|
||||||
|
SpaceNode equipment = spaceNodeRepository.findByIdAndIsDeletedFalse(failure.getEquipmentId())
|
||||||
|
.orElseThrow(() -> new BusinessException(6001, "设备不存在"));
|
||||||
|
|
||||||
|
if (!Boolean.TRUE.equals(equipment.getIsEquipment())) {
|
||||||
|
throw new BusinessException(6002, "该空间节点不是设备");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置项目ID
|
||||||
|
if (failure.getProjectId() == null) {
|
||||||
|
failure.setProjectId(equipment.getProjectId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置设备信息
|
||||||
|
if (failure.getEquipmentCode() == null) {
|
||||||
|
failure.setEquipmentCode(equipment.getCode());
|
||||||
|
}
|
||||||
|
if (failure.getEquipmentName() == null) {
|
||||||
|
failure.setEquipmentName(equipment.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算修复时长
|
||||||
|
if (failure.getRepairStartTime() != null && failure.getRepairEndTime() != null) {
|
||||||
|
long minutes = ChronoUnit.MINUTES.between(failure.getRepairStartTime(), failure.getRepairEndTime());
|
||||||
|
double hours = minutes / 60.0;
|
||||||
|
failure.setRepairDurationHours(hours);
|
||||||
|
|
||||||
|
// 计算停机时长(如果故障时间和修复开始时间不同)
|
||||||
|
if (failure.getFailureTime() != null && failure.getRepairStartTime().isAfter(failure.getFailureTime())) {
|
||||||
|
long downtimeMinutes = ChronoUnit.MINUTES.between(failure.getFailureTime(), failure.getRepairStartTime());
|
||||||
|
failure.setDowntimeHours(downtimeMinutes / 60.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return failureHistoryRepository.save(failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EquipmentHealthScore> getHealthHistory(UUID equipmentId) {
|
||||||
|
return healthScoreRepository.findByEquipmentIdOrderByCalculatedAtDesc(equipmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EquipmentHealthScore getLatestHealthScore(UUID equipmentId) {
|
||||||
|
return healthScoreRepository.findLatestByEquipmentId(equipmentId)
|
||||||
|
.orElseThrow(() -> new BusinessException(6003, "没有健康度记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EquipmentFailureHistory> getFailureHistory(UUID equipmentId) {
|
||||||
|
return failureHistoryRepository.findByEquipmentIdOrderByFailureTimeDesc(equipmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算设备年龄(年)
|
||||||
|
*/
|
||||||
|
private BigDecimal calculateEquipmentAge(SpaceNode equipment) {
|
||||||
|
LocalDate installationDate = equipment.getMaintenanceContractStart();
|
||||||
|
if (installationDate == null) {
|
||||||
|
// 如果没有安装日期,使用创建日期
|
||||||
|
if (equipment.getCreatedAt() != null) {
|
||||||
|
installationDate = equipment.getCreatedAt().toLocalDate();
|
||||||
|
} else {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Period period = Period.between(installationDate, LocalDate.now());
|
||||||
|
int years = period.getYears();
|
||||||
|
int months = period.getMonths();
|
||||||
|
|
||||||
|
return BigDecimal.valueOf(years).add(BigDecimal.valueOf(months).divide(BigDecimal.valueOf(12), 2, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue