# 设施设备管理增强功能开发计划

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** 实现非居物业设施设备管理增强功能（M02-13~18），包括设备技术参数扩展、预防性维护引擎、能耗监控、备件库存、故障预测、点检标准库

**Architecture:** 基于现有 ether-pms 单体架构，在 module-mdm 模块新增设备管理相关实体和服务，前端新增设备管理页面。能耗监控和故障预测作为独立功能模块开发。

**Tech Stack:** Spring Boot 3.x + JPA + PostgreSQL + Vue3 + TypeScript + Ant Design Vue

---

## 开发阶段总览

```
阶段一（M02-13）：设备技术参数扩展     - 约3天
阶段二（M02-14）：预防性维护引擎      - 约5天
阶段三（M02-15）：能耗监控管理         - 约4天
阶段四（M02-16）：备件库存管理         - 约4天
阶段五（M02-17）：设备故障预测         - 约3天
阶段六（M02-18）：设备点检标准库       - 约3天
─────────────────────────────────────────
总计                                      约22天
```

---

## 阶段一：M02-13 设备技术参数扩展

### 数据库迁移

**文件:** `ether-pms/src/main/resources/db/migration/V10__add_equipment_extension_fields.sql`

```sql
-- 添加设备扩展字段到 mdm_space_node 表（设备关联空间节点）
ALTER TABLE mdm_space_node ADD COLUMN design_life_years INTEGER;
ALTER TABLE mdm_space_node ADD COLUMN rated_power DECIMAL(10,2);
ALTER TABLE mdm_space_node ADD COLUMN rated_voltage VARCHAR(20);
ALTER TABLE mdm_space_node ADD COLUMN rated_current DECIMAL(10,2);
ALTER TABLE mdm_space_node ADD COLUMN maintenance_vendor VARCHAR(100);
ALTER TABLE mdm_space_node ADD COLUMN maintenance_vendor_contact VARCHAR(50);
ALTER TABLE mdm_space_node ADD COLUMN maintenance_vendor_phone VARCHAR(20);
ALTER TABLE mdm_space_node ADD COLUMN maintenance_contract_no VARCHAR(50);
ALTER TABLE mdm_space_node ADD COLUMN maintenance_contract_start DATE;
ALTER TABLE mdm_space_node ADD COLUMN maintenance_contract_end DATE;
ALTER TABLE mdm_space_node ADD COLUMN special_equipment_type VARCHAR(50);
ALTER TABLE mdm_space_node ADD COLUMN special_equipment_cert VARCHAR(100);
ALTER TABLE mdm_space_node ADD COLUMN inspection_cycle INTEGER;
ALTER TABLE mdm_space_node ADD COLUMN next_inspection_date DATE;
ALTER TABLE mdm_space_node ADD COLUMN last_inspection_date DATE;
ALTER TABLE mdm_space_node ADD COLUMN last_inspection_result VARCHAR(20);
ALTER TABLE mdm_space_node ADD COLUMN common_spare_parts JSONB;
ALTER TABLE mdm_space_node ADD COLUMN energy_consumption_standard DECIMAL(12,2);
ALTER TABLE mdm_space_node ADD COLUMN installation_environment VARCHAR(50);
ALTER TABLE mdm_space_node ADD COLUMN protection_level VARCHAR(20);
```

---

### Task 1.1: 创建设备分类枚举

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/enums/EquipmentCategory.java`

```java
package com.ether.pms.mdm.enums;

public enum EquipmentCategory {
    HVAC("暖通空调"),
    ELECTRICAL("电气设备"),
    FIRE("消防设备"),
    ELEVATOR("电梯设备"),
    SECURITY("安防设备"),
   给排水("给排水设备"),
    LIGHTING("照明设备"),
    SPECIAL("特种设备");

    private final String desc;
    EquipmentCategory(String desc) { this.desc = desc; }
    public String getDesc() { return desc; }
}
```

**Step 1:** 创建枚举类
**Step 2:** 验证编译通过
**Step 3:** Commit: `feat: add equipment category enum`

---

### Task 1.2: 创建设备类型枚举

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/enums/EquipmentType.java`

```java
package com.ether.pms.mdm.enums;

public enum EquipmentType {
    CENTRAL_AC("中央空调", EquipmentCategory.HVAC),
    AIR_CONDITIONER("分体空调", EquipmentCategory.HVAC),
    AIR_HANDLING_UNIT("空气处理机组", EquipmentCategory.HVAC),
    FAN_COIL("风机盘管", EquipmentCategory.HVAC),
    LOW_VOLTAGE_CABINET("低压配电柜", EquipmentCategory.ELECTRICAL),
    TRANSFORMER("变压器", EquipmentCategory.ELECTRICAL),
    GENERATOR("发电机", EquipmentCategory.ELECTRICAL),
    UPS("不间断电源", EquipmentCategory.ELECTRICAL),
    FIRE_PUMP("消防泵", EquipmentCategory.FIRE),
    SPRINKLER("喷淋系统", EquipmentCategory.FIRE),
    FIRE_ALARM("火灾报警系统", EquipmentCategory.FIRE),
    ELEVATOR("电梯", EquipmentCategory.ELEVATOR),
    CCTV("监控系统", EquipmentCategory.SECURITY),
    ACCESS_CONTROL("门禁系统", EquipmentCategory.SECURITY),
    WATER_PUMP("给水泵", EquipmentCategory.WATER_DRAINAGE),
    DRAINAGE_PUMP("排水泵", EquipmentCategory.WATER_DRAINAGE),
    LED_LIGHT("LED灯具", EquipmentCategory.LIGHTING),
    HIGH_BAY_LIGHT("工矿灯", EquipmentCategory.LIGHTING);

    private final String desc;
    private final EquipmentCategory category;
    EquipmentType(String desc, EquipmentCategory category) {
        this.desc = desc;
        this.category = category;
    }
    public String getDesc() { return desc; }
    public EquipmentCategory getCategory() { return category; }
}
```

**Step 1:** 创建枚举类
**Step 2:** 验证编译通过
**Step 3:** Commit: `feat: add equipment type enum`

---

### Task 1.3: 更新SpaceNode实体

**Files:**
- Modify: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java`

在现有实体末尾添加扩展字段：

```java
// ========== 设备扩展字段 ==========
@Column(name = "is_equipment")
private Boolean isEquipment = false;

@Column(name = "design_life_years")
private Integer designLifeYears;

@Column(name = "rated_power", precision = 10, scale = 2)
private BigDecimal ratedPower;

@Column(name = "rated_voltage", length = 20)
private String ratedVoltage;

@Column(name = "rated_current", precision = 10, scale = 2)
private BigDecimal ratedCurrent;

@Column(name = "maintenance_vendor", length = 100)
private String maintenanceVendor;

@Column(name = "maintenance_vendor_contact", length = 50)
private String maintenanceVendorContact;

@Column(name = "maintenance_vendor_phone", length = 20)
private String maintenanceVendorPhone;

@Column(name = "maintenance_contract_no", length = 50)
private String maintenanceContractNo;

@Column(name = "maintenance_contract_start")
private LocalDate maintenanceContractStart;

@Column(name = "maintenance_contract_end")
private LocalDate maintenanceContractEnd;

@Column(name = "special_equipment_type", length = 50)
private String specialEquipmentType;

@Column(name = "special_equipment_cert", length = 100)
private String specialEquipmentCert;

@Column(name = "inspection_cycle")
private Integer inspectionCycle;

@Column(name = "next_inspection_date")
private LocalDate nextInspectionDate;

@Column(name = "last_inspection_date")
private LocalDate lastInspectionDate;

@Column(name = "last_inspection_result", length = 20)
private String lastInspectionResult;

@Column(name = "common_spare_parts", columnDefinition = "TEXT")
private String commonSpareParts;

@Column(name = "energy_consumption_standard", precision = 12, scale = 2)
private BigDecimal energyConsumptionStandard;

@Column(name = "installation_environment", length = 50)
private String installationEnvironment;

@Column(name = "protection_level", length = 20)
private String protectionLevel;
// ========== 设备扩展字段结束 ==========
```

**Step 1:** 添加扩展字段到 SpaceNode 实体
**Step 2:** 运行 `cd ether-pms && mvn compile` 验证
**Step 3:** Commit: `feat: add equipment extension fields to SpaceNode`

---

### Task 1.4: 创建SpaceNode DTO

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeEquipmentDTO.java`

```java
package com.ether.pms.mdm.dto;

import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

@Data
@EqualsAndHashCode(callSuper = true)
public class SpaceNodeEquipmentDTO extends SpaceNodeDTO {

    private Boolean isEquipment;
    private Integer designLifeYears;
    private BigDecimal ratedPower;
    private String ratedVoltage;
    private BigDecimal ratedCurrent;
    private String maintenanceVendor;
    private String maintenanceVendorContact;
    private String maintenanceVendorPhone;
    private String maintenanceContractNo;
    private LocalDate maintenanceContractStart;
    private LocalDate maintenanceContractEnd;
    private String specialEquipmentType;
    private String specialEquipmentCert;
    private Integer inspectionCycle;
    private LocalDate nextInspectionDate;
    private LocalDate lastInspectionDate;
    private String lastInspectionResult;
    private List<SparePartInfo> commonSpareParts;
    private BigDecimal energyConsumptionStandard;
    private String installationEnvironment;
    private String protectionLevel;

    @Data
    public static class SparePartInfo {
        private String name;
        private String model;
        private Integer quantity;
    }
}
```

**Step 1:** 创建 DTO 类
**Step 2:** 运行 `cd ether-pms && mvn compile` 验证
**Step 3:** Commit: `feat: add SpaceNodeEquipmentDTO`

---

### Task 1.5: 更新SpaceNodeService

**Files:**
- Modify: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/SpaceNodeService.java`

添加设备相关方法：

```java
public SpaceNodeEquipmentDTO getEquipmentById(UUID id) {
    SpaceNode node = spaceNodeRepository.findById(id)
        .orElseThrow(() -> new BusinessException("EQUIPMENT_NOT_FOUND"));
    return convertToEquipmentDTO(node);
}

public List<SpaceNodeEquipmentDTO> getSpecialEquipmentList(UUID projectId) {
    List<SpaceNode> list = spaceNodeRepository.findByProjectIdAndIsEquipmentAndSpecialEquipmentTypeIsNotNull(
        projectId, true);
    return list.stream().map(this::convertToEquipmentDTO).collect(Collectors.toList());
}

public List<SpaceNodeEquipmentDTO> getExpiringInspectionEquipment(UUID projectId, Integer daysAhead) {
    LocalDate threshold = LocalDate.now().plusDays(daysAhead);
    List<SpaceNode> list = spaceNodeRepository
        .findByProjectIdAndIsEquipmentAndNextInspectionDateBefore(projectId, true, threshold);
    return list.stream().map(this::convertToEquipmentDTO).collect(Collectors.toList());
}
```

**Step 1:** 添加服务方法
**Step 2:** 添加单元测试
**Step 3:** 运行测试验证
**Step 4:** Commit: `feat: add equipment service methods`

---

### Task 1.6: 更新SpaceNodeController

**Files:**
- Modify: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/controller/SpaceNodeController.java`

添加设备相关API：

```java
@GetMapping("/{id}/equipment")
public ApiResponse<SpaceNodeEquipmentDTO> getEquipment(@PathVariable UUID id) {
    return ApiResponse.success(spaceNodeService.getEquipmentById(id));
}

@GetMapping("/special-equipment")
public ApiResponse<List<SpaceNodeEquipmentDTO>> getSpecialEquipment(
    @RequestParam UUID projectId) {
    return ApiResponse.success(spaceNodeService.getSpecialEquipmentList(projectId));
}

@GetMapping("/expiring-inspection")
public ApiResponse<List<SpaceNodeEquipmentDTO>> getExpiringInspection(
    @RequestParam UUID projectId,
    @RequestParam(defaultValue = "90") Integer daysAhead) {
    return ApiResponse.success(spaceNodeService.getExpiringInspectionEquipment(projectId, daysAhead));
}
```

**Step 1:** 添加控制器端点
**Step 2:** 使用 curl 测试端点
**Step 3:** Commit: `feat: add equipment API endpoints`

---

### Task 1.7: 前端 - 创建设备管理页面

**Files:**
- Create: `ether-admin/src/views/equipment/EquipmentList.vue`
- Create: `ether-admin/src/views/equipment/EquipmentDetail.vue`
- Create: `ether-admin/src/api/equipment.ts`

**Step 1:** 创建 API 模块 `ether-admin/src/api/equipment.ts`

```typescript
import request from '@/utils/request'

export interface EquipmentForm {
  id?: string
  code: string
  name: string
  nodeType: string
  locationDesc?: string
  designLifeYears?: number
  ratedPower?: number
  ratedVoltage?: string
  maintenanceVendor?: string
  maintenanceVendorPhone?: string
  specialEquipmentType?: string
  inspectionCycle?: number
  nextInspectionDate?: string
  [key: string]: any
}

export function getEquipmentList(params: any) {
  return request({
    url: '/api/v1/mdm/space-nodes/equipment',
    method: 'get',
    params
  })
}

export function getEquipmentDetail(id: string) {
  return request({
    url: `/api/v1/mdm/space-nodes/${id}/equipment`,
    method: 'get'
  })
}

export function getSpecialEquipment(projectId: string) {
  return request({
    url: '/api/v1/mdm/space-nodes/special-equipment',
    method: 'get',
    params: { projectId }
  })
}

export function getExpiringInspection(projectId: string, daysAhead?: number) {
  return request({
    url: '/api/v1/mdm/space-nodes/expiring-inspection',
    method: 'get',
    params: { projectId, daysAhead }
  })
}
```

**Step 2:** 创建设备列表页面 `EquipmentList.vue`

**Step 3:** 创建设备详情页面 `EquipmentDetail.vue`

**Step 4:** 运行 `npm run build` 验证前端编译

**Step 5:** Commit: `feat: add equipment management pages`

---

## 阶段二：M02-14 预防性维护引擎

### Task 2.1: 创建维保计划实体

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/MaintenancePlan.java`

```java
package com.ether.pms.mdm.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Table(name = "ops_maintenance_plan")
@Data
public class MaintenancePlan {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "project_id", nullable = false)
    private UUID projectId;

    @Column(name = "plan_code", nullable = false, unique = true)
    private String planCode;

    @Column(name = "plan_name", nullable = false)
    private String planName;

    @Column(name = "equipment_type")
    private String equipmentType;

    @Column(name = "trigger_type", nullable = false)
    @Enumerated(EnumType.STRING)
    private TriggerType triggerType;

    public enum TriggerType {
        TIME_BASED,    // 时间触发
        HOURS_BASED,   // 运行小时触发
        CYCLES_BASED,  // 次数触发
        CONDITION_BASED // 条件触发
    }

    @Column(name = "trigger_value")
    private Integer triggerValue;

    @Column(name = "trigger_unit")
    private String triggerUnit;

    @Column(name = "maintenance_items", columnDefinition = "TEXT")
    private String maintenanceItems;

    @Column(name = "estimated_duration")
    private Integer estimatedDuration;

    @Column(name = "assigned_to")
    private UUID assignedTo;

    @Column(name = "sla_response_hours")
    private Integer slaResponseHours;

    @Column(name = "sla_complete_hours")
    private Integer slaCompleteHours;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private Status status = Status.ACTIVE;

    public enum Status {
        ACTIVE, INACTIVE
    }

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdate() {
        updatedAt = LocalDateTime.now();
    }
}
```

**Step 1:** 创建实体类
**Step 2:** 运行 `mvn compile` 验证
**Step 3:** Commit: `feat: add MaintenancePlan entity`

---

### Task 2.2: 创建维保任务实体

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/MaintenanceTask.java`

```java
package com.ether.pms.mdm.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Table(name = "ops_maintenance_task")
@Data
public class MaintenanceTask {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "project_id", nullable = false)
    private UUID projectId;

    @Column(name = "task_code", nullable = false, unique = true)
    private String taskCode;

    @Column(name = "plan_id")
    private UUID planId;

    @Column(name = "equipment_id")
    private UUID equipmentId;

    @Column(name = "task_type")
    @Enumerated(EnumType.STRING)
    private TaskType taskType = TaskType.PREVENTIVE;

    public enum TaskType {
        PREVENTIVE,   // 预防性维护
        CORRECTIVE    // 纠正性维护
    }

    @Column(name = "trigger_type")
    @Enumerated(EnumType.STRING)
    private MaintenancePlan.TriggerType triggerType;

    @Column(name = "maintenance_items", columnDefinition = "TEXT")
    private String maintenanceItems;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private Status status = Status.PENDING;

    public enum Status {
        PENDING,      // 待接受
        ACCEPTED,     // 已接受
        IN_PROGRESS,  // 执行中
        COMPLETED,    // 已完成
        CANCELLED    // 已取消
    }

    @Column(name = "assigned_to")
    private UUID assignedTo;

    @Column(name = "scheduled_date")
    private LocalDateTime scheduledDate;

    @Column(name = "actual_start_date")
    private LocalDateTime actualStartDate;

    @Column(name = "actual_end_date")
    private LocalDateTime actualEndDate;

    @Column(name = "labor_hours", precision = 10, scale = 2)
    private BigDecimal laborHours;

    @Column(name = "materials_cost", precision = 12, scale = 2)
    private BigDecimal materialsCost;

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

    @PreUpdate
    public void preUpdate() {
        updatedAt = LocalDateTime.now();
    }
}
```

**Step 1:** 创建实体类
**Step 2:** 运行 `mvn compile` 验证
**Step 3:** Commit: `feat: add MaintenanceTask entity`

---

### Task 2.3: 创建维保Repository

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/repository/MaintenancePlanRepository.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/repository/MaintenanceTaskRepository.java`

```java
package com.ether.pms.mdm.repository;

public interface MaintenancePlanRepository extends JpaRepository<MaintenancePlan, UUID> {
    List<MaintenancePlan> findByProjectIdAndStatus(UUID projectId, MaintenancePlan.Status status);
    Optional<MaintenancePlan> findByPlanCode(String planCode);
}

public interface MaintenanceTaskRepository extends JpaRepository<MaintenanceTask, UUID> {
    List<MaintenanceTask> findByProjectIdAndStatus(UUID projectId, MaintenanceTask.Status status);
    List<MaintenanceTask> findByAssignedToAndStatus(UUID assignedTo, MaintenanceTask.Status status);
    List<MaintenanceTask> findByEquipmentIdAndStatusNot(UUID equipmentId, MaintenanceTask.Status status);
    Optional<MaintenanceTask> findByTaskCode(String taskCode);
}
```

**Step 1:** 创建 Repository
**Step 2:** 添加查询方法单元测试
**Step 3:** Commit: `feat: add maintenance repositories`

---

### Task 2.4: 创建维保Service

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/MaintenancePlanService.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/MaintenanceTaskService.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/MaintenancePlanServiceImpl.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/MaintenanceTaskServiceImpl.java`

**Service核心方法：**

```java
// MaintenancePlanService
UUID createPlan(MaintenancePlan plan);
List<MaintenancePlan> getActivePlansByProject(UUID projectId);
void updatePlan(UUID id, MaintenancePlan plan);
void deactivatePlan(UUID id);

// MaintenanceTaskService
List<MaintenanceTask> getTasksByAssignee(UUID assignee, String status);
void acceptTask(UUID taskId, UUID userId);
void startTask(UUID taskId);
void completeTask(UUID taskId, BigDecimal laborHours, BigDecimal materialsCost, String remarks);
List<MaintenanceTask> getOverdueTasks();
```

**Step 1:** 创建 Service 接口和实现
**Step 2:** 添加单元测试
**Step 3:** 运行测试验证
**Step 4:** Commit: `feat: add maintenance service`

---

### Task 2.5: 创建维保Controller

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/controller/MaintenancePlanController.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/controller/MaintenanceTaskController.java`

**API 设计：**

| 方法 | 路径 | 描述 |
|------|------|------|
| POST | /api/v1/ops/maintenance-plans | 创建维保计划 |
| GET | /api/v1/ops/maintenance-plans | 获取维保计划列表 |
| PUT | /api/v1/ops/maintenance-plans/{id} | 更新维保计划 |
| DELETE | /api/v1/ops/maintenance-plans/{id} | 删除维保计划 |
| GET | /api/v1/ops/maintenance-tasks | 获取维保任务列表 |
| POST | /api/v1/ops/maintenance-tasks/{id}/accept | 接受任务 |
| POST | /api/v1/ops/maintenance-tasks/{id}/start | 开始执行 |
| POST | /api/v1/ops/maintenance-tasks/{id}/complete | 完成维保 |

**Step 1:** 创建 Controller
**Step 2:** 使用 curl 测试 API
**Step 3:** Commit: `feat: add maintenance controllers`

---

### Task 2.6: 创建定时任务 - 维保触发检查

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/scheduler/MaintenanceScheduler.java`

```java
package com.ether.pms.mdm.scheduler;

@Component
@RequiredArgsConstructor
public class MaintenanceScheduler {

    private final MaintenancePlanService planService;
    private final MaintenanceTaskService taskService;

    @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点
    public void checkTimeBasedMaintenance() {
        log.info("检查时间触发的维保计划...");
        List<MaintenancePlan> plans = planService.getTimeBasedActivePlans();
        plans.forEach(plan -> taskService.generateTasksFromPlan(plan));
    }

    @Scheduled(cron = "0 0 * * * ?") // 每小时
    public void checkHourlyMaintenance() {
        log.info("检查运行小时触发的维保计划...");
        // 检查运行小时触发的计划
    }
}
```

**Step 1:** 创建定时任务类
**Step 2:** 在 PmsApplication 添加 `@EnableScheduling`
**Step 3:** 测试定时任务执行
**Step 4:** Commit: `feat: add maintenance scheduler`

---

## 阶段三：M02-15 能耗监控管理

### Task 3.1: 创建能耗相关实体

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/EnergyMeter.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/EnergyConsumption.java`

```java
@Entity
@Table(name = "ops_energy_meter")
@Data
public class EnergyMeter {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "project_id", nullable = false)
    private UUID projectId;

    @Column(name = "meter_code", nullable = false, unique = true)
    private String meterCode;

    @Column(name = "meter_name", nullable = false)
    private String meterName;

    @Column(name = "energy_type", nullable = false)
    @Enumerated(EnumType.STRING)
    private EnergyType energyType;

    public enum EnergyType {
        LIGHTING, // 照明插座用电
        HVAC,     // 空调用电
        POWER,    // 动力用电
        SPECIAL,  // 特殊用电
        WATER,    // 给排水
        GAS       // 燃气
    }

    @Column(name = "space_node_id")
    private UUID spaceNodeId;

    @Column(name = "installation_location")
    private String installationLocation;

    @Column(name = "unit_price", precision = 10, scale = 4)
    private BigDecimal unitPrice;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private Status status = Status.ACTIVE;

    public enum Status { ACTIVE, INACTIVE }
}

@Entity
@Table(name = "ops_energy_consumption")
@Data
public class EnergyConsumption {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "project_id", nullable = false)
    private UUID projectId;

    @Column(name = "meter_id", nullable = false)
    private UUID meterId;

    @Column(name = "consumption_date", nullable = false)
    private LocalDate consumptionDate;

    @Column(name = "previous_reading", precision = 12, scale = 2)
    private BigDecimal previousReading;

    @Column(name = "current_reading", precision = 12, scale = 2)
    private BigDecimal currentReading;

    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal consumption;

    @Column(precision = 10, scale = 2)
    private BigDecimal amount;

    @Column(name = "recorded_by")
    private UUID recordedBy;

    @Column(name = "record_method")
    @Enumerated(EnumType.STRING)
    private RecordMethod recordMethod = RecordMethod.MANUAL;

    public enum RecordMethod { MANUAL, IOT }
}
```

**Step 1:** 创建实体
**Step 2:** 运行编译验证
**Step 3:** Commit: `feat: add energy management entities`

---

### Task 3.2: 创建能耗Service

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/EnergyMeterService.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/EnergyConsumptionService.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/EnergyStatisticsService.java`

```java
// EnergyConsumptionService
void recordConsumption(UUID meterId, BigDecimal currentReading, UUID recordedBy);
List<EnergyConsumption> getConsumptionByMeter(UUID meterId, LocalDate startDate, LocalDate endDate);

// EnergyStatisticsService
Map<EnergyType, BigDecimal> getConsumptionByType(UUID projectId, LocalDate month);
BigDecimal getUnitConsumption(UUID projectId, LocalDate month); // kWh/m²
Map<String, BigDecimal> getConsumptionTrend(UUID projectId, Integer months);
List<EnergyAlert> getAlerts(UUID projectId);
```

**Step 1:** 创建 Service
**Step 2:** 添加单元测试
**Step 3:** Commit: `feat: add energy service`

---

### Task 3.3: 创建能耗Controller

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/controller/EnergyController.java`

**API 设计：**

| 方法 | 路径 | 描述 |
|------|------|------|
| GET | /api/v1/ops/energy-meters | 获取计量点列表 |
| POST | /api/v1/ops/energy-meters | 创建计量点 |
| POST | /api/v1/ops/energy-consumption | 录入能耗数据 |
| GET | /api/v1/ops/energy-statistics/by-type | 按分项统计 |
| GET | /api/v1/ops/energy-statistics/unit-consumption | 单方能耗统计 |
| GET | /api/v1/ops/energy-statistics/trend | 能耗趋势 |

**Step 1:** 创建 Controller
**Step 2:** 测试 API
**Step 3:** Commit: `feat: add energy controller`

---

## 阶段四：M02-16 备件库存管理

### Task 4.1: 创建备件实体

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/SparePart.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/SparePartRecord.java`

```java
@Entity
@Table(name = "ops_spare_part")
@Data
public class SparePart {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "project_id", nullable = false)
    private UUID projectId;

    @Column(name = "spare_part_code", nullable = false, unique = true)
    private String sparePartCode;

    @Column(name = "spare_part_name", nullable = false)
    private String sparePartName;

    @Column(name = "category_id")
    private UUID categoryId;

    @Column
    private String specification;

    @Column(nullable = false)
    private String unit;

    @Column(name = "safe_stock")
    private Integer safeStock;

    @Column(name = "current_stock")
    private Integer currentStock;

    @Column(name = "unit_price", precision = 10, scale = 2)
    private BigDecimal unitPrice;

    @Column
    private String supplier;

    @Column(name = "location")
    private String location;

    @Column
    @Enumerated(EnumType.STRING)
    private Status status = Status.ACTIVE;
}

@Entity
@Table(name = "ops_spare_part_record")
@Data
public class SparePartRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "record_code", nullable = false, unique = true)
    private String recordCode;

    @Column(name = "record_type", nullable = false)
    @Enumerated(EnumType.STRING)
    private RecordType recordType;

    public enum RecordType {
        IN,      // 入库
        OUT,     // 出库
        CHECK,   // 盘点
        ADJUST   // 调整
    }

    @Column(name = "spare_part_id", nullable = false)
    private UUID sparePartId;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false)
    private Integer balance;

    @Column(name = "related_order_id")
    private UUID relatedOrderId;

    @Column(name = "recorded_by")
    private UUID recordedBy;

    @Column(name = "record_date")
    private LocalDateTime recordDate;
}
```

**Step 1:** 创建实体
**Step 2:** 编译验证
**Step 3:** Commit: `feat: add spare part entities`

---

### Task 4.2: 创建备件Service

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/SparePartService.java`

```java
public interface SparePartService {
    SparePart create(SparePart sparePart);
    void updateStock(UUID sparePartId, Integer quantity, SparePartRecord.RecordType type, UUID relatedOrderId);
    List<SparePart> getLowStockParts(UUID projectId);
    List<SparePartRecord> getRecordsByPart(UUID sparePartId);
}
```

**Step 1:** 创建 Service
**Step 2:** 添加库存扣减逻辑（事务处理）
**Step 3:** 添加单元测试
**Step 4:** Commit: `feat: add spare part service`

---

### Task 4.3: 创建备件Controller

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/controller/SparePartController.java`

**API 设计：**

| 方法 | 路径 | 描述 |
|------|------|------|
| GET | /api/v1/ops/spare-parts | 获取备件列表 |
| POST | /api/v1/ops/spare-parts | 创建备件 |
| GET | /api/v1/ops/spare-parts/{id} | 获取备件详情 |
| GET | /api/v1/ops/spare-parts/low-stock | 获取低库存备件 |
| POST | /api/v1/ops/spare-part-records | 创建备件记录 |
| GET | /api/v1/ops/spare-part-records/{sparePartId} | 获取备件记录 |

**Step 1:** 创建 Controller
**Step 2:** 测试 API
**Step 3:** Commit: `feat: add spare part controller`

---

## 阶段五：M02-17 设备故障预测

### Task 5.1: 创建故障历史实体

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/EquipmentFailureHistory.java`
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/EquipmentHealthScore.java`

```java
@Entity
@Table(name = "ops_equipment_failure_history")
@Data
public class EquipmentFailureHistory {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "equipment_id", nullable = false)
    private UUID equipmentId;

    @Column(name = "failure_date", nullable = false)
    private LocalDateTime failureDate;

    @Column(name = "failure_type")
    private String failureType;

    @Column(name = "failure_cause")
    private String failureCause;

    @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", precision = 10, scale = 2)
    private BigDecimal repairDuration;

    @Column(name = "repair_cost", precision = 12, scale = 2)
    private BigDecimal repairCost;

    @Column(name = "spare_parts_used", columnDefinition = "TEXT")
    private String sparePartsUsed;

    @Column(name = "work_order_id")
    private UUID workOrderId;
}

@Entity
@Table(name = "ops_equipment_health_score")
@Data
public class EquipmentHealthScore {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "equipment_id", nullable = false)
    private UUID equipmentId;

    @Column(name = "score_date", nullable = false)
    private LocalDate scoreDate;

    @Column(name = "health_score", precision = 5, scale = 2)
    private BigDecimal healthScore;

    @Column(precision = 10, scale = 2)
    private BigDecimal mtbf;

    @Column(precision = 10, scale = 2)
    private BigDecimal mttr;

    @Column(name = "failure_count")
    private Integer failureCount;

    @Column(name = "maintenance_completion_rate", precision = 5, scale = 2)
    private BigDecimal maintenanceCompletionRate;

    @Column(name = "alert_level")
    @Enumerated(EnumType.STRING)
    private AlertLevel alertLevel;

    public enum AlertLevel { NORMAL, WARNING, CRITICAL }
}
```

**Step 1:** 创建实体
**Step 2:** 编译验证
**Step 3:** Commit: `feat: add equipment health entities`

---

### Task 5.2: 创建故障预测Service

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/EquipmentHealthService.java`

```java
public interface EquipmentHealthService {

    // 计算设备健康度
    EquipmentHealthScore calculateHealthScore(UUID equipmentId);

    // 获取设备MTBF
    BigDecimal calculateMTBF(UUID equipmentId, Integer days);

    // 获取设备MTTR
    BigDecimal calculateMTTR(UUID equipmentId, Integer days);

    // 获取设备健康度历史
    List<EquipmentHealthScore> getHealthHistory(UUID equipmentId);

    // 获取预警设备列表
    List<UUID> getAlertEquipmentIds(UUID projectId, AlertLevel level);

    // 记录故障
    void recordFailure(EquipmentFailureHistory failure);
}
```

**健康度算法实现：**
```java
public EquipmentHealthScore calculateHealthScore(UUID equipmentId) {
    // 基础分 = 100
    BigDecimal baseScore = new BigDecimal("100");

    // 故障率扣分 = 故障次数 × 5（近30天）
    int failureCount = failureHistoryRepository.countByEquipmentIdAndFailureDateAfter(
        equipmentId, LocalDateTime.now().minusDays(30));
    BigDecimal failureDeduction = new BigDecimal(failureCount * 5);

    // 维保扣分 = (1 - 维保完成率) × 20
    BigDecimal maintenanceRate = maintenanceTaskService.getCompletionRate(equipmentId);
    BigDecimal maintenanceDeduction = new BigDecimal("1").subtract(maintenanceRate)
        .multiply(new BigDecimal("20"));

    // 年龄扣分 = 投入使用年限 × 2（最高扣10分）
    SpaceNode equipment = spaceNodeRepository.findById(equipmentId).orElseThrow();
    int yearsInService = calculateYearsInService(equipment.getCreatedAt());
    BigDecimal ageDeduction = new BigDecimal(Math.min(yearsInService * 2, 10));

    BigDecimal healthScore = baseScore
        .subtract(failureDeduction)
        .subtract(maintenanceDeduction)
        .subtract(ageDeduction);

    return healthScore;
}
```

**Step 1:** 创建 Service 接口
**Step 2:** 实现计算逻辑
**Step 3:** 添加单元测试
**Step 4:** Commit: `feat: add equipment health service`

---

## 阶段六：M02-18 设备点检标准库

### Task 6.1: 创建点检模板实体

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/InspectionTemplate.java`

```java
@Entity
@Table(name = "ops_inspection_template")
@Data
public class InspectionTemplate {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "project_id", nullable = false)
    private UUID projectId;

    @Column(name = "template_code", nullable = false, unique = true)
    private String templateCode;

    @Column(name = "template_name", nullable = false)
    private String templateName;

    @Column(name = "equipment_type")
    private String equipmentType;

    @Column(name = "inspection_items", columnDefinition = "TEXT")
    private String inspectionItems;

    @Column(name = "estimated_duration")
    private Integer estimatedDuration;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private Status status = Status.ACTIVE;

    public enum Status { ACTIVE, INACTIVE }

    @Column
    private Integer version;

    @Column(name = "created_by")
    private UUID createdBy;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
        if (version == null) version = 1;
    }
}
```

**Step 1:** 创建实体
**Step 2:** 编译验证
**Step 3:** Commit: `feat: add inspection template entity`

---

### Task 6.2: 创建点检Service

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/service/InspectionTemplateService.java`

```java
public interface InspectionTemplateService {
    InspectionTemplate create(InspectionTemplate template);
    InspectionTemplate copyTemplate(UUID templateId, String newName);
    List<InspectionTemplate> getTemplatesByType(String equipmentType);
    void updateTemplate(UUID id, InspectionTemplate template);
}
```

**点检项目JSON结构：**
```json
{
  "items": [
    {
      "itemCode": "I001",
      "itemName": "电机温度检查",
      "inspectionPoint": "电机外壳",
      "inspectionMethod": "HAND_FEEL",
      "standard": "温度正常，无明显过热",
      "normalValue": "≤60°C",
      "abnormalValue": ">70°C",
      "handlingMethod": "停机检查，联系维修",
      "isRequired": true
    }
  ]
}
```

**Step 1:** 创建 Service
**Step 2:** 添加 JSON 解析/存储逻辑
**Step 3:** Commit: `feat: add inspection template service`

---

### Task 6.3: 创建点检Controller

**Files:**
- Create: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionTemplateController.java`

**API 设计：**

| 方法 | 路径 | 描述 |
|------|------|------|
| GET | /api/v1/ops/inspection-templates | 获取点检模板列表 |
| POST | /api/v1/ops/inspection-templates | 创建点检模板 |
| GET | /api/v1/ops/inspection-templates/{id} | 获取模板详情 |
| PUT | /api/v1/ops/inspection-templates/{id} | 更新模板 |
| POST | /api/v1/ops/inspection-templates/{id}/copy | 复制模板 |
| GET | /api/v1/ops/inspection-templates/by-type/{equipmentType} | 按设备类型获取 |

**Step 1:** 创建 Controller
**Step 2:** 测试 API
**Step 3:** Commit: `feat: add inspection template controller`

---

## 前端页面开发（统一在最后）

### Task F.1: 设备管理页面

**Files:**
- Create: `ether-admin/src/views/equipment/EquipmentList.vue`
- Create: `ether-admin/src/views/equipment/EquipmentDetail.vue`
- Create: `ether-admin/src/views/equipment/EquipmentForm.vue`
- Modify: `ether-admin/src/views/Layout.vue` - 添加设备管理菜单

**Step 1:** 创建设备列表页面
**Step 2:** 创建设备详情页面
**Step 3:** 添加设备表单组件
**Step 4:** 更新 Layout 添加菜单
**Step 5:** 运行 `npm run build` 验证
**Step 6:** Commit: `feat: add equipment management frontend`

---

### Task F.2: 维保管理页面

**Files:**
- Create: `ether-admin/src/views/maintenance/PlanList.vue`
- Create: `ether-admin/src/views/maintenance/TaskList.vue`

**Step 1:** 创建维保计划列表
**Step 2:** 创建维保任务列表
**Step 3:** 编译验证
**Step 4:** Commit: `feat: add maintenance management frontend`

---

### Task F.3: 能耗监控页面

**Files:**
- Create: `ether-admin/src/views/energy/EnergyMeterList.vue`
- Create: `ether-admin/src/views/energy/EnergyStatistics.vue`

**Step 1:** 创建计量点管理页面
**Step 2:** 创建能耗统计页面
**Step 3:** 编译验证
**Step 4:** Commit: `feat: add energy monitoring frontend`

---

### Task F.4: 备件管理页面

**Files:**
- Create: `ether-admin/src/views/sparepart/SparePartList.vue`
- Create: `ether-admin/src/views/sparepart/SparePartRecord.vue`

**Step 1:** 创建备件列表页面
**Step 2:** 创建备件记录页面
**Step 3:** 编译验证
**Step 4:** Commit: `feat: add spare part management frontend`

---

## 测试验证

### Task T.1: 后端单元测试

为每个 Service 创建单元测试：
- `SpaceNodeServiceTest`
- `MaintenancePlanServiceTest`
- `MaintenanceTaskServiceTest`
- `EnergyConsumptionServiceTest`
- `SparePartServiceTest`
- `EquipmentHealthServiceTest`

```bash
cd ether-pms && mvn test -Dtest=*ServiceTest
```

---

### Task T.2: 后端集成测试

```bash
# 使用 H2 内存数据库进行集成测试
cd ether-pms && mvn verify
```

---

### Task T.3: 前端构建验证

```bash
cd ether-admin && npm run build
```

---

### Task T.4: E2E 测试

使用 Playwright 进行 E2E 测试：

```bash
# 设备管理 E2E
npx playwright test tests/equipment.spec.ts

# 维保管理 E2E
npx playwright test tests/maintenance.spec.ts
```

---

## 数据库迁移脚本汇总

**文件列表：**
1. `V10__add_equipment_extension_fields.sql` - 设备扩展字段
2. `V11__create_maintenance_tables.sql` - 维保计划和任务表
3. `V12__create_energy_tables.sql` - 能耗表
4. `V13__create_spare_part_tables.sql` - 备件表
5. `V14__create_equipment_health_tables.sql` - 设备健康表
6. `V15__create_inspection_template_table.sql` - 点检模板表

---

## 部署检查清单

- [ ] 数据库迁移脚本执行
- [ ] 后端编译通过
- [ ] 后端单元测试通过
- [ ] 前端编译通过
- [ ] API 端点验证
- [ ] E2E 测试通过
- [ ] 文档更新

---

## 风险与缓解

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 定时任务性能 | 维保检查任务执行慢 | 使用 @Async 或分布式任务 |
| 能耗数据量大 | 查询性能问题 | 添加索引，分区表 |
| IoT 数据接入 | 协议兼容问题 | 预留适配器接口 |
| 历史数据不足 | 故障预测不准 | 初期使用规则判断 |

---

*计划版本: v1.0*
*最后更新: 2026-03-23*
