# 运营与服务域 - 详细设计

**文档编号**: DETAIL-OPS
**版本**: v1.0
**模块**: module-wo + module-mdm（备件/能耗部分）
**生成日期**: 2026-05-18
**数据来源**: ether-pms/module-wo + module-mdm 实际代码 + REVERSE-OPS.md 反推文档

---

## 一、功能点清单

| 功能ID | 功能名称 | 优先级 | 实现状态 | 对应需求ID |
|--------|----------|--------|----------|------------|
| OPS-001 | 工单创建 | P0 | 已完成 | 02-OPERATIONS-001 |
| OPS-002 | 工单查询（分页） | P0 | 已完成 | 02-OPERATIONS-001 |
| OPS-003 | 工单详情 | P0 | 已完成 | 02-OPERATIONS-001 |
| OPS-004 | 工单更新 | P1 | 已完成 | 02-OPERATIONS-001 |
| OPS-005 | 工单删除（逻辑删除） | P1 | 已完成 | 02-OPERATIONS-001 |
| OPS-006 | 工单派单 | P0 | 已完成 | 02-OPERATIONS-002 |
| OPS-007 | 工单开始执行 | P0 | 已完成 | 02-OPERATIONS-002 |
| OPS-008 | 工单完成 | P0 | 已完成 | 02-OPERATIONS-002 |
| OPS-009 | 工单验收 | P0 | 已完成 | 02-OPERATIONS-002 |
| OPS-010 | 工单取消 | P1 | 已完成 | 02-OPERATIONS-002 |
| OPS-011 | 工单挂起 | P1 | 已完成 | 02-OPERATIONS-002 |
| OPS-012 | 工单恢复 | P1 | 已完成 | 02-OPERATIONS-002 |
| OPS-013 | 工单退回 | P1 | 已完成 | 02-OPERATIONS-002 |
| OPS-014 | 工单统计 | P1 | 已完成 | 02-OPERATIONS-003 |
| OPS-015 | 工单明细查询 | P1 | 已完成 | 02-OPERATIONS-004 |
| OPS-016 | 工单明细批量添加 | P1 | 已完成 | 02-OPERATIONS-004 |
| OPS-017 | 维保任务创建 | P0 | 已完成 | 02-OPERATIONS-005 |
| OPS-018 | 维保任务查询 | P0 | 已完成 | 02-OPERATIONS-005 |
| OPS-019 | 维保任务分配 | P0 | 已完成 | 02-OPERATIONS-005 |
| OPS-020 | 维保任务开始执行 | P0 | 已完成 | 02-OPERATIONS-005 |
| OPS-021 | 维保任务完成（简版） | P0 | 已完成 | 02-OPERATIONS-005 |
| OPS-022 | 维保任务完成（详版） | P0 | 已完成 | 02-OPERATIONS-005 |
| OPS-023 | 维保任务验收 | P0 | 已完成 | 02-OPERATIONS-005 |
| OPS-024 | 维保任务取消 | P1 | 已完成 | 02-OPERATIONS-005 |
| OPS-025 | 维保任务评分 | P2 | 已完成 | 02-OPERATIONS-005 |
| OPS-026 | 维保任务统计 | P1 | 已完成 | 02-OPERATIONS-006 |
| OPS-027 | 维保计划管理 | P1 | 已完成 | 02-OPERATIONS-007 |
| OPS-028 | 巡检模板管理 | P1 | 已完成 | 02-OPERATIONS-008 |
| OPS-029 | 巡检模板复制 | P2 | 已完成 | 02-OPERATIONS-008 |
| OPS-030 | 备件CRUD | P0 | 已完成 | 02-OPERATIONS-009 |
| OPS-031 | 备件分类管理 | P1 | 已完成 | 02-OPERATIONS-009 |
| OPS-032 | 备件入库 | P0 | 已完成 | 02-OPERATIONS-010 |
| OPS-033 | 备件出库 | P0 | 已完成 | 02-OPERATIONS-010 |
| OPS-034 | 备件低库存预警 | P1 | 已完成 | 02-OPERATIONS-010 |
| OPS-035 | 备件出入库记录 | P1 | 已完成 | 02-OPERATIONS-010 |
| OPS-036 | 能源计量点管理 | P1 | 已完成 | 02-OPERATIONS-011 |
| OPS-037 | 能耗抄表记录 | P0 | 已完成 | 02-OPERATIONS-012 |
| OPS-038 | 能耗按类型统计 | P1 | 已完成 | 02-OPERATIONS-013 |
| OPS-039 | 单位面积能耗 | P2 | 已完成 | 02-OPERATIONS-013 |
| OPS-040 | 消息通知系统 | P1 | 未实现 | 02-OPERATIONS-014 |
| OPS-041 | 工单流转记录 | P2 | 未实现 | 02-OPERATIONS-015 |
| OPS-042 | SLA监控 | P2 | 未实现 | 02-OPERATIONS-016 |
| OPS-043 | 智能派单 | P3 | 未实现 | 02-OPERATIONS-017 |

---

## 二、数据结构设计

### 2.1 WorkOrder（工单）

**表名**: `ops_work_order`
**实体类**: `com.ether.pms.ops.entity.WorkOrder`

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| workNo | VARCHAR(50) | 是 | 自动生成 | 工单编号，格式: WO-YYYYMMDD-XXXX，UNIQUE |
| source | ENUM | 是 | - | 工单来源（6种） |
| type | ENUM | 是 | - | 工单类型（6种） |
| priority | ENUM | 否 | MEDIUM | 优先级（4级） |
| status | ENUM | 否 | PENDING | 工单状态（8种） |
| previousStatus | ENUM | 否 | - | 前一状态（挂起恢复用） |
| title | VARCHAR(200) | 是 | - | 工单标题 |
| description | VARCHAR(2000) | 否 | - | 工单描述 |
| projectId | UUID | 是 | - | 所属项目 |
| equipmentId | UUID | 否 | - | 关联设备 |
| spaceId | UUID | 否 | - | 关联空间 |
| planId | UUID | 否 | - | 关联维保计划 |
| triggerType | ENUM | 否 | - | 触发类型（4种） |
| assignedTo | VARCHAR(200) | 否 | - | 负责人 |
| assignedVendor | VARCHAR(200) | 否 | - | 服务商 |
| assignedDate | DATE | 否 | - | 派单日期 |
| actualStart | TIMESTAMP | 否 | - | 实际开始时间 |
| actualEnd | TIMESTAMP | 否 | - | 实际结束时间 |
| actualHours | DECIMAL(6,2) | 否 | - | 实际工时（小时） |
| faultCause | VARCHAR(2000) | 否 | - | 故障原因 |
| solution | VARCHAR(5000) | 否 | - | 解决方案 |
| result | VARCHAR(2000) | 否 | - | 处理结果 |
| laborCost | DECIMAL(12,2) | 否 | - | 人工费 |
| partsCost | DECIMAL(12,2) | 否 | - | 备件费 |
| totalCost | DECIMAL(12,2) | 否 | - | 总费用 |
| completedBy | VARCHAR(200) | 否 | - | 完成人 |
| completedDate | DATE | 否 | - | 完成日期 |
| verifiedBy | VARCHAR(200) | 否 | - | 验收人 |
| verifiedDate | DATE | 否 | - | 验收日期 |
| rating | INTEGER | 否 | - | 评分（1-5星） |
| remark | VARCHAR(1000) | 否 | - | 备注 |
| photos | JSONB | 否 | - | 照片URL列表(List\<String\>) |
| signature | VARCHAR(2000) | 否 | - | 签名 |
| isDeleted | BOOLEAN | 否 | false | 逻辑删除标记 |
| createdAt | TIMESTAMP | 是 | now() | 创建时间 |
| updatedAt | TIMESTAMP | 是 | now() | 更新时间 |
| createdBy | VARCHAR(200) | 否 | - | 创建人 |

**枚举定义**:

| 枚举 | 值 | 中文 |
|------|-----|------|
| Source | OWNER | 业主报修 |
| | MAINTENANCE | 维保计划 |
| | INSPECTION | 巡检触发 |
| | FAULT | 故障触发 |
| | REGULATORY | 法规巡检 |
| | MANUAL | 手动创建 |
| Type | REPAIR | 维修 |
| | INSPECTION | 巡检 |
| | SECURITY | 安保 |
| | CLEANING | 保洁 |
| | PROPERTY | 物业 |
| | CONSULTATION | 咨询 |
| Priority | LOW / MEDIUM / HIGH / URGENT | 低/中/高/紧急 |
| Status | PENDING / ASSIGNED / IN_PROGRESS / SUSPENDED / RETURNED / COMPLETED / VERIFIED / CANCELLED | 见状态机 |
| TriggerType | PLAN / INSPECTION / FAULT / MANUAL | 计划/巡检/故障/手动 |

**索引**:

| 索引名 | 字段 | 用途 |
|--------|------|------|
| idx_wo_project_status | project_id, status | 项目+状态查询 |
| idx_wo_priority_status | priority, status | 优先级+状态查询 |
| idx_wo_plan_createdat | plan_id, created_at | 计划关联查询 |
| idx_wo_status_createdat | status, created_at | 状态+时间查询 |
| idx_wo_createdat_desc | created_at DESC | 时间倒序 |

### 2.2 WorkOrderItem（工单明细）

**表名**: `ops_work_order_item`
**实体类**: `com.ether.pms.ops.entity.WorkOrderItem`

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| workOrderId | UUID | 是 | - | 所属工单ID |
| itemType | ENUM | 是 | - | 明细类型（3种） |
| itemName | VARCHAR(200) | 是 | - | 明细名称 |
| quantity | DECIMAL(10,2) | 否 | 1 | 数量 |
| unit | VARCHAR(50) | 否 | - | 单位 |
| unitPrice | DECIMAL(12,2) | 否 | - | 单价 |
| totalPrice | DECIMAL(12,2) | 否 | - | 总价 |
| isNormal | BOOLEAN | 否 | - | 是否正常 |
| observation | VARCHAR(2000) | 否 | - | 观察记录 |
| suggestion | VARCHAR(2000) | 否 | - | 建议 |
| sortOrder | INTEGER | 否 | 0 | 排序序号 |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |

**枚举定义**:

| 枚举 | 值 | 中文 |
|------|-----|------|
| ItemType | PART | 备件 |
| | INSPECTION_ITEM | 巡检项 |
| | CHECKPOINT | 检查点 |

### 2.3 MaintenancePlan（维保计划）

**表名**: `ops_maintenance_plan`
**实体类**: `com.ether.pms.ops.entity.MaintenancePlan`

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| planCode | VARCHAR(50) | 是 | 自动生成 | 计划编码（UNIQUE） |
| planName | VARCHAR(200) | 是 | - | 计划名称 |
| planContent | VARCHAR(5000) | 否 | - | 计划内容 |
| projectId | UUID | 否 | - | 所属项目 |
| equipmentId | UUID | 是 | - | 关联设备 |
| planType | ENUM | 是 | - | 计划类型（2种） |
| cycleDays | INTEGER | 否 | - | 周期天数 |
| estimatedHours | DECIMAL(6,2) | 否 | - | 预估工时 |
| assignedVendor | VARCHAR(200) | 否 | - | 指定服务商 |
| status | ENUM | 否 | ACTIVE | 计划状态（3种） |
| lastDate | DATE | 否 | - | 上次执行日期 |
| nextDate | DATE | 否 | - | 下次执行日期 |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |
| updatedAt | TIMESTAMP | 否 | now() | 更新时间 |
| createdBy | VARCHAR(200) | 否 | - | 创建人 |

**枚举定义**:

| 枚举 | 值 | 中文 |
|------|-----|------|
| PlanType | PREVENTIVE | 预防性维护 |
| | CORRECTIVE | 纠正性维护 |
| PlanStatus | ACTIVE | 启用 |
| | INACTIVE | 停用 |
| | SUSPENDED | 暂停 |

### 2.4 MaintenanceTask（维保任务）

**表名**: `ops_maintenance_task`
**实体类**: `com.ether.pms.ops.entity.MaintenanceTask`

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| taskNo | VARCHAR(50) | 否 | 自动生成 | 任务编号，格式: EQ-YYYYMMDD-XXXX，UNIQUE |
| planId | UUID | 否 | - | 关联维保计划ID |
| equipmentId | UUID | 是 | - | 关联设备ID |
| projectId | UUID | 否 | - | 所属项目 |
| taskType | ENUM | 否 | - | 任务类型（3种） |
| triggerType | ENUM | 否 | - | 触发类型（4种） |
| priority | ENUM | 否 | 自动判定 | 优先级（4级） |
| status | ENUM | 否 | PENDING | 任务状态（6种） |
| title | VARCHAR(200) | 否 | - | 任务标题 |
| description | VARCHAR(2000) | 否 | - | 任务描述 |
| assignedTo | VARCHAR(200) | 否 | - | 负责人 |
| assignedVendor | VARCHAR(200) | 否 | - | 服务商 |
| assignedDate | DATE | 否 | - | 派单日期 |
| actualStart | TIMESTAMP | 否 | - | 实际开始时间 |
| actualEnd | TIMESTAMP | 否 | - | 实际结束时间 |
| actualHours | DECIMAL(6,2) | 否 | - | 实际工时 |
| faultCause | VARCHAR(2000) | 否 | - | 故障原因 |
| solution | VARCHAR(5000) | 否 | - | 解决方案 |
| result | VARCHAR(2000) | 否 | - | 处理结果 |
| partsUsed | JSONB | 否 | - | 使用备件列表(List\<Map\>) |
| laborCost | DECIMAL(12,2) | 否 | - | 人工费 |
| partsCost | DECIMAL(12,2) | 否 | - | 备件费 |
| totalCost | DECIMAL(12,2) | 否 | - | 总费用 |
| completedBy | VARCHAR(200) | 否 | - | 完成人 |
| completedDate | DATE | 否 | - | 完成日期 |
| verifiedBy | VARCHAR(200) | 否 | - | 验收人 |
| verifiedDate | DATE | 否 | - | 验收日期 |
| rating | INTEGER | 否 | - | 评分（1-5） |
| remark | VARCHAR(1000) | 否 | - | 备注 |
| photos | JSONB | 否 | - | 照片URL列表 |
| signature | VARCHAR(2000) | 否 | - | 签名 |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |
| updatedAt | TIMESTAMP | 否 | now() | 更新时间 |
| createdBy | VARCHAR(200) | 否 | - | 创建人 |

**枚举定义**:

| 枚举 | 值 | 中文 |
|------|-----|------|
| TaskType | PREVENTIVE | 预防性维护 |
| | CORRECTIVE | 纠正性维护 |
| | EMERGENCY | 紧急维修 |
| TriggerType | PLAN | 计划触发 |
| | INSPECTION | 巡检触发 |
| | FAULT | 故障触发 |
| | MANUAL | 手动创建 |
| Priority | LOW / MEDIUM / HIGH / URGENT | 低/中/高/紧急 |
| Status | PENDING / ASSIGNED / IN_PROGRESS / COMPLETED / VERIFIED / CANCELLED | 见状态机 |

**partsUsed JSONB 结构**:

```json
[
  {
    "partsId": "uuid",
    "partsName": "备件名称",
    "quantity": 2,
    "unitPrice": 100.00,
    "totalPrice": 200.00
  }
]
```

**索引**:

| 索引名 | 字段 | 用途 |
|--------|------|------|
| idx_mt_equipment_status | equipment_id, status | 设备+状态查询 |
| idx_mt_project_status | project_id, status | 项目+状态查询 |
| idx_mt_plan_createdat | plan_id, created_at | 计划关联查询 |
| idx_mt_status_assigneddate | status, assigned_date | 状态+派单日期查询 |

### 2.5 InspectionTemplate（巡检模板）

**表名**: `ops_inspection_template`
**实体类**: `com.ether.pms.mdm.entity.InspectionTemplate`（位于module-mdm）

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| templateCode | VARCHAR(50) | 是 | 自动生成 | 模板编码（UNIQUE） |
| templateName | VARCHAR(200) | 是 | - | 模板名称 |
| description | VARCHAR(2000) | 否 | - | 描述 |
| projectId | UUID | 否 | - | 所属项目 |
| spaceId | UUID | 否 | - | 关联空间 |
| category | VARCHAR(50) | 否 | - | 分类 |
| status | ENUM | 否 | ACTIVE | 模板状态（2种） |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |
| updatedAt | TIMESTAMP | 否 | now() | 更新时间 |
| createdBy | VARCHAR(200) | 否 | - | 创建人 |
| items | List\<InspectionItem\> | - | - | 检查项列表（@Transient，非持久化） |

**枚举定义**:

| 枚举 | 值 | 中文 |
|------|-----|------|
| TemplateStatus | ACTIVE | 启用 |
| | INACTIVE | 停用 |

### 2.6 InspectionItem（巡检项）

**表名**: `ops_inspection_item`
**实体类**: `com.ether.pms.mdm.entity.InspectionItem`（位于module-mdm）

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| templateId | UUID | 是 | - | 所属模板ID |
| itemName | VARCHAR(200) | 是 | - | 检查项名称 |
| description | VARCHAR(2000) | 否 | - | 描述 |
| checkMethod | VARCHAR(2000) | 否 | - | 检查方法 |
| standard | VARCHAR(2000) | 否 | - | 检查标准 |
| isMandatory | BOOLEAN | 否 | true | 是否必检 |
| isNormalRequired | BOOLEAN | 否 | true | 是否需要正常判定 |
| sortOrder | INTEGER | 否 | 0 | 排序序号 |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |

### 2.7 SparePart（备件）

**表名**: `ops_spare_part`
**实体类**: `com.ether.pms.mdm.entity.SparePart`（位于module-mdm）

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| projectId | UUID | 是 | - | 所属项目 |
| sparePartCode | VARCHAR | 是 | 自动生成 | 备件编码（UNIQUE），格式: SP+yyyyMMddHHmmss |
| sparePartName | VARCHAR | 是 | - | 备件名称 |
| categoryId | UUID | 否 | - | 分类ID |
| specification | VARCHAR(500) | 否 | - | 规格 |
| unit | VARCHAR(50) | 是 | - | 单位 |
| safeStock | INTEGER | 否 | 0 | 安全库存 |
| currentStock | INTEGER | 否 | 0 | 当前库存 |
| unitPrice | DECIMAL(10,2) | 否 | - | 单价 |
| supplier | VARCHAR(200) | 否 | - | 供应商 |
| supplierContact | VARCHAR | 否 | - | 供应商联系方式 |
| location | VARCHAR(200) | 否 | - | 存放位置 |
| remarks | VARCHAR(1000) | 否 | - | 备注 |
| status | ENUM | 是 | ACTIVE | 状态（2种） |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |
| updatedAt | TIMESTAMP | 否 | now() | 更新时间 |

**枚举定义**:

| 枚举 | 值 | 中文 |
|------|-----|------|
| Status | ACTIVE | 启用 |
| | INACTIVE | 停用 |

**索引**:

| 索引名 | 字段 | 用途 |
|--------|------|------|
| idx_sp_project_status | project_id, status | 项目+状态查询 |

### 2.8 SparePartCategory（备件分类）

**表名**: `ops_spare_part_category`
**实体类**: `com.ether.pms.mdm.entity.SparePartCategory`（位于module-mdm）

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| parentId | UUID | 否 | - | 父分类ID（支持树形结构） |
| categoryCode | VARCHAR | 是 | 自动生成 | 分类编码（UNIQUE），格式: CC+yyyyMMddHHmmss |
| categoryName | VARCHAR | 是 | - | 分类名称 |
| description | VARCHAR(500) | 否 | - | 描述 |
| sortOrder | INTEGER | 否 | - | 排序序号 |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |

### 2.9 SparePartRecord（备件出入库记录）

**表名**: `ops_spare_part_record`
**实体类**: `com.ether.pms.mdm.entity.SparePartRecord`（位于module-mdm）

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| recordCode | VARCHAR | 是 | 自动生成 | 记录编码（UNIQUE），格式: REC+类型+yyyyMMddHHmmss |
| recordType | ENUM | 是 | - | 记录类型（4种） |
| sparePartId | UUID | 是 | - | 备件ID |
| quantity | INTEGER | 是 | - | 数量 |
| balance | INTEGER | 是 | - | 操作后余额 |
| relatedOrderId | UUID | 否 | - | 关联工单ID |
| recordedBy | UUID | 否 | - | 操作人 |
| recordDate | TIMESTAMP | 否 | now() | 操作时间 |
| remarks | VARCHAR(1000) | 否 | - | 备注 |

**枚举定义**:

| 枚举 | 值 | 中文 | 说明 |
|------|-----|------|------|
| RecordType | IN | 入库 | 库存增加 |
| | OUT | 出库 | 库存减少，可关联工单 |
| | CHECK | 盘点 | 库存校准 |
| | ADJUST | 调整 | 库存调整 |

### 2.10 EnergyMeter（能源计量点）

**表名**: `ops_energy_meter`
**实体类**: `com.ether.pms.mdm.entity.EnergyMeter`（位于module-mdm）

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| projectId | UUID | 是 | - | 所属项目 |
| meterCode | VARCHAR | 是 | 自动生成 | 计量点编码（UNIQUE） |
| meterName | VARCHAR | 是 | - | 计量点名称 |
| energyType | ENUM | 是 | - | 能源类型（6种） |
| spaceNodeId | UUID | 否 | - | 关联空间节点 |
| installationLocation | VARCHAR | 否 | - | 安装位置 |
| ratedCapacity | DECIMAL(10,2) | 否 | - | 额定容量 |
| unitPrice | DECIMAL(10,4) | 否 | - | 单价 |
| status | ENUM | 是 | ACTIVE | 状态（2种） |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |
| updatedAt | TIMESTAMP | 否 | now() | 更新时间 |

**枚举定义**:

| 枚举 | 值 | 中文 | 说明 |
|------|-----|------|------|
| EnergyType | LIGHTING | 照明插座用电 | 建筑分项能耗分类 |
| | HVAC | 空调用电 | 建筑分项能耗分类 |
| | POWER | 动力用电 | 建筑分项能耗分类 |
| | SPECIAL | 特殊用电 | 建筑分项能耗分类 |
| | WATER | 给排水 | 水耗计量 |
| | GAS | 燃气 | 气耗计量 |
| Status | ACTIVE | 启用 | 可正常抄表 |
| | INACTIVE | 停用 | 不可抄表 |

### 2.11 EnergyConsumption（能耗记录）

**表名**: `ops_energy_consumption`
**实体类**: `com.ether.pms.mdm.entity.EnergyConsumption`（位于module-mdm）

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| id | UUID | 是 | 自动生成 | 主键 |
| projectId | UUID | 是 | - | 项目ID |
| meterId | UUID | 是 | - | 计量点ID |
| consumptionDate | DATE | 是 | - | 消耗日期 |
| previousReading | DECIMAL(12,2) | 否 | - | 上次读数 |
| currentReading | DECIMAL(12,2) | 否 | - | 当前读数 |
| consumption | DECIMAL(12,2) | 是 | - | 消耗量 = currentReading - previousReading |
| amount | DECIMAL(10,2) | 否 | - | 费用 = consumption x unitPrice |
| recordedBy | UUID | 否 | - | 抄表人 |
| recordMethod | ENUM | 否 | MANUAL | 抄表方式（2种） |
| remarks | VARCHAR(1000) | 否 | - | 备注 |
| createdAt | TIMESTAMP | 否 | now() | 创建时间 |

**枚举定义**:

| 枚举 | 值 | 中文 | 说明 |
|------|-----|------|------|
| RecordMethod | MANUAL | 手动录入 | 人工抄表 |
| | IOT | IoT自动采集 | 自动采集（预留） |

**索引**:

| 索引名 | 字段 | 用途 |
|--------|------|------|
| idx_ec_meter_date | meter_id, consumption_date | 计量点+日期查询 |
| idx_ec_project_date | project_id, consumption_date | 项目+日期查询 |

### 2.12 ER关系图

```mermaid
erDiagram
    MaintenancePlan ||--o{ MaintenanceTask : "1:N planId"
    MaintenancePlan }o--|| Equipment : "equipmentId"
    WorkOrder ||--o{ WorkOrderItem : "1:N workOrderId"
    WorkOrder }o--o| MaintenancePlan : "planId"
    WorkOrder }o--o| Equipment : "equipmentId"
    InspectionTemplate ||--o{ InspectionItem : "1:N templateId"
    SparePartCategory ||--o{ SparePart : "1:N categoryId"
    SparePart ||--o{ SparePartRecord : "1:N sparePartId"
    EnergyMeter ||--o{ EnergyConsumption : "1:N meterId"

    WorkOrder {
        UUID id PK
        String workNo UK
        Source source
        Type type
        Priority priority
        Status status
        Status previousStatus
        UUID projectId FK
        UUID equipmentId FK
        UUID planId FK
    }

    WorkOrderItem {
        UUID id PK
        UUID workOrderId FK
        ItemType itemType
        String itemName
        BigDecimal quantity
        BigDecimal totalPrice
    }

    MaintenancePlan {
        UUID id PK
        String planCode UK
        String planName
        PlanType planType
        Integer cycleDays
        PlanStatus status
        UUID equipmentId FK
    }

    MaintenanceTask {
        UUID id PK
        String taskNo UK
        UUID planId FK
        UUID equipmentId FK
        TaskType taskType
        TriggerType triggerType
        Priority priority
        Status status
    }

    InspectionTemplate {
        UUID id PK
        String templateCode UK
        String templateName
        String category
        TemplateStatus status
    }

    InspectionItem {
        UUID id PK
        UUID templateId FK
        String itemName
        String checkMethod
        String standard
        Boolean isMandatory
    }

    SparePart {
        UUID id PK
        String sparePartCode UK
        String sparePartName
        UUID categoryId FK
        Integer currentStock
        Integer safeStock
    }

    SparePartRecord {
        UUID id PK
        String recordCode UK
        RecordType recordType
        UUID sparePartId FK
        Integer quantity
        Integer balance
        UUID relatedOrderId FK
    }

    EnergyMeter {
        UUID id PK
        String meterCode UK
        String meterName
        EnergyType energyType
        BigDecimal unitPrice
    }

    EnergyConsumption {
        UUID id PK
        UUID meterId FK
        LocalDate consumptionDate
        BigDecimal consumption
        BigDecimal amount
    }
```

---

## 三、API设计

### 3.1 WorkOrderController

**基础路径**: `/api/wo/work-orders`
**控制器类**: `com.ether.pms.ops.controller.WorkOrderController`

#### 工单 CRUD

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| POST | `/` | 创建工单 | WorkOrder | ApiResponse\<WorkOrder\> | OPS-001 |
| GET | `/` | 查询工单列表（分页） | QueryParams | ApiResponse\<PageResponse\<WorkOrder\>\> | OPS-002 |
| GET | `/{id}` | 获取工单详情 | - | ApiResponse\<WorkOrder\> | OPS-003 |
| PUT | `/{id}` | 更新工单 | WorkOrder | ApiResponse\<WorkOrder\> | OPS-004 |
| DELETE | `/{id}` | 删除工单（逻辑删除） | - | ApiResponse\<Void\> | OPS-005 |

**查询参数**（GET /）:

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| projectId | UUID | 否 | - | 按项目筛选 |
| equipmentId | UUID | 否 | - | 按设备筛选 |
| source | Source | 否 | - | 按来源筛选 |
| type | Type | 否 | - | 按类型筛选 |
| status | Status | 否 | - | 按状态筛选 |
| priority | Priority | 否 | - | 按优先级筛选 |
| assignedTo | String | 否 | - | 按负责人筛选 |
| keyword | String | 否 | - | 关键词搜索 |
| page | int | 否 | 0 | 页码 |
| size | int | 否 | 20 | 每页大小 |

#### 工单状态流转

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| POST | `/{id}/assign` | 派单 | AssignRequest | ApiResponse\<WorkOrder\> | OPS-006 |
| POST | `/{id}/start` | 开始执行 | - | ApiResponse\<WorkOrder\> | OPS-007 |
| POST | `/{id}/complete` | 完成工单 | WorkOrder | ApiResponse\<WorkOrder\> | OPS-008 |
| POST | `/{id}/verify` | 验收工单 | VerifyRequest | ApiResponse\<WorkOrder\> | OPS-009 |
| POST | `/{id}/cancel` | 取消工单 | - | ApiResponse\<WorkOrder\> | OPS-010 |
| POST | `/{id}/suspend` | 挂起工单 | - | ApiResponse\<WorkOrder\> | OPS-011 |
| POST | `/{id}/resume` | 恢复工单 | - | ApiResponse\<WorkOrder\> | OPS-012 |
| POST | `/{id}/return` | 退回工单 | - | ApiResponse\<WorkOrder\> | OPS-013 |

**请求体定义**:

```java
// 派单请求
AssignRequest {
    String assignedTo;       // 负责人
    String assignedVendor;   // 服务商
    LocalDate assignedDate;  // 派单日期
}

// 验收请求
VerifyRequest {
    String verifiedBy;  // 验收人
    String remark;      // 备注
    Integer rating;     // 评分（1-5）
}
```

#### 工单统计与明细

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| GET | `/stats` | 工单统计 | - | ApiResponse\<WorkOrderStatsDTO\> | OPS-014 |
| GET | `/{id}/items` | 获取工单明细 | - | ApiResponse\<List\<WorkOrderItem\>\> | OPS-015 |
| POST | `/{id}/items` | 批量添加工单明细 | List\<WorkOrderItem\> | ApiResponse\<WorkOrder\> | OPS-016 |

**WorkOrderStatsDTO**:

| 字段 | 类型 | 说明 |
|------|------|------|
| total | long | 工单总数 |
| pending | long | 待分配数 |
| assigned | long | 已派单数 |
| inProgress | long | 执行中数 |
| completed | long | 已完成数 |
| verified | long | 已验收数 |
| cancelled | long | 已取消数 |
| suspended | long | 已挂起数 |
| returned | long | 已退回数 |
| completedToday | long | 今日完成数 |
| createdToday | long | 今日创建数 |
| overdue | long | 逾期数 |
| bySource | Map\<String, Long\> | 按来源分布 |
| byType | Map\<String, Long\> | 按类型分布 |
| byPriority | Map\<String, Long\> | 按优先级分布 |

### 3.2 MaintenanceTaskController

**基础路径**: `/api/ops/maintenance-tasks`
**控制器类**: `com.ether.pms.ops.controller.MaintenanceTaskController`

#### 维保任务 CRUD

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| POST | `/` | 创建维保任务 | MaintenanceTask | ApiResponse\<MaintenanceTask\> | OPS-017 |
| GET | `/` | 查询维保任务列表 | QueryParams | ApiResponse\<List\<MaintenanceTask\>\> | OPS-018 |
| GET | `/{id}` | 获取任务详情 | - | ApiResponse\<MaintenanceTask\> | OPS-018 |
| PUT | `/{id}` | 更新任务 | MaintenanceTask | ApiResponse\<MaintenanceTask\> | OPS-018 |
| DELETE | `/{id}` | 删除任务（逻辑删除，设为CANCELLED） | - | ApiResponse\<Void\> | OPS-024 |

**查询参数**（GET /）:

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| equipmentId | UUID | 否 | 按设备筛选 |
| planId | UUID | 否 | 按计划筛选 |
| status | Status | 否 | 按状态筛选 |
| priority | Priority | 否 | 按优先级筛选 |
| assignedTo | String | 否 | 按负责人筛选 |
| overdueDate | LocalDate | 否 | 逾期截止日期 |

> 查询参数互斥优先级：equipmentId > planId > status > priority > assignedTo > overdueDate

#### 维保任务状态流转

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| POST | `/{id}/assign` | 分配任务 | AssignRequest | ApiResponse\<MaintenanceTask\> | OPS-019 |
| POST | `/{id}/start` | 开始执行 | - | ApiResponse\<MaintenanceTask\> | OPS-020 |
| POST | `/{id}/complete` | 完成任务（简版） | CompleteRequest | ApiResponse\<MaintenanceTask\> | OPS-021 |
| POST | `/{id}/complete-details` | 完成任务（详版） | MaintenanceTask | ApiResponse\<MaintenanceTask\> | OPS-022 |
| POST | `/{id}/verify` | 验收任务 | VerifyRequest | ApiResponse\<MaintenanceTask\> | OPS-023 |
| POST | `/{id}/cancel` | 取消任务 | - | ApiResponse\<MaintenanceTask\> | OPS-024 |
| POST | `/{id}/rate` | 评分 | rating (query param) | ApiResponse\<MaintenanceTask\> | OPS-025 |

**请求体定义**:

```java
// 分配请求
AssignRequest {
    String assignedTo;
    LocalDate assignedDate;
}

// 完成请求（简版）
CompleteRequest {
    String result;
    BigDecimal actualHours;
    BigDecimal cost;
    String completedBy;
}

// 验收请求
VerifyRequest {
    String verifiedBy;
    String remark;
    Integer rating;
}
```

#### 维保任务统计

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| GET | `/stats` | 任务统计 | - | ApiResponse\<MaintenanceTaskStatsDTO\> | OPS-026 |

**MaintenanceTaskStatsDTO**:

| 字段 | 类型 | 说明 |
|------|------|------|
| total | long | 任务总数 |
| pending | long | 待分配数 |
| assigned | long | 已派单数 |
| inProgress | long | 执行中数 |
| completed | long | 已完成数 |
| verified | long | 已验收数 |
| cancelled | long | 已取消数 |
| completedToday | long | 今日完成数 |
| createdToday | long | 今日创建数 |
| overdue | long | 逾期数 |
| avgCompleteHours | BigDecimal | 平均完成工时 |
| avgRating | BigDecimal | 平均评分 |
| byPriority | Map\<String, Long\> | 按优先级分布 |
| byTriggerType | Map\<String, Long\> | 按触发类型分布 |

### 3.3 InspectionTemplateController

**基础路径**: `/api/ops/inspection-templates`
**控制器类**: `com.ether.pms.mdm.controller.InspectionTemplateController`（位于module-mdm）

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| GET | `/` | 按项目查询模板 | projectId | ApiResponse\<List\<InspectionTemplate\>\> | OPS-028 |
| POST | `/` | 创建模板 | InspectionTemplate | ApiResponse\<InspectionTemplate\> | OPS-028 |
| GET | `/{id}` | 获取模板详情 | - | ApiResponse\<InspectionTemplate\> | OPS-028 |
| PUT | `/{id}` | 更新模板 | InspectionTemplate | ApiResponse\<InspectionTemplate\> | OPS-028 |
| POST | `/{id}/copy` | 复制模板 | newName (query param) | ApiResponse\<InspectionTemplate\> | OPS-029 |
| GET | `/by-type/{equipmentType}` | 按设备类型查询 | - | ApiResponse\<List\<InspectionTemplate\>\> | OPS-028 |

### 3.4 SparePartController

**基础路径**: `/api/ops/spare-parts`
**控制器类**: `com.ether.pms.mdm.controller.SparePartController`（位于module-mdm）

#### 分类管理

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| GET | `/categories` | 获取所有分类 | - | ApiResponse\<List\<SparePartCategory\>\> | OPS-031 |
| POST | `/categories` | 创建分类 | SparePartCategory | ApiResponse\<SparePartCategory\> | OPS-031 |

#### 备件管理

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| GET | `/` | 查询备件列表 | projectId, categoryId(可选) | ApiResponse\<List\<SparePart\>\> | OPS-030 |
| GET | `/{id}` | 获取备件详情 | - | ApiResponse\<SparePart\> | OPS-030 |
| POST | `/` | 创建备件 | SparePart | ApiResponse\<SparePart\> | OPS-030 |
| PUT | `/{id}` | 更新备件 | SparePart | ApiResponse\<SparePart\> | OPS-030 |
| DELETE | `/{id}` | 删除备件（设为INACTIVE） | - | ApiResponse\<Void\> | OPS-030 |
| GET | `/low-stock` | 低库存预警 | projectId | ApiResponse\<List\<SparePart\>\> | OPS-034 |

#### 库存操作

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| POST | `/in-stock` | 入库 | StockRequest | ApiResponse\<SparePartRecord\> | OPS-032 |
| POST | `/out-stock` | 出库 | OutStockRequest | ApiResponse\<SparePartRecord\> | OPS-033 |
| GET | `/{id}/records` | 获取出入库记录 | - | ApiResponse\<List\<SparePartRecord\>\> | OPS-035 |

**请求体定义**:

```java
// 入库请求
StockRequest {
    @NotNull UUID sparePartId;
    @NotNull Integer quantity;
    UUID recordedBy;
    String remarks;
}

// 出库请求
OutStockRequest {
    @NotNull UUID sparePartId;
    @NotNull Integer quantity;
    UUID relatedOrderId;    // 关联工单ID
    UUID recordedBy;
    String remarks;
}
```

### 3.5 EnergyController

**基础路径**: `/api/ops/energy`
**控制器类**: `com.ether.pms.mdm.controller.EnergyController`（位于module-mdm）

#### 计量点管理

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| POST | `/meters` | 创建计量点 | EnergyMeter | ApiResponse\<EnergyMeter\> | OPS-036 |
| GET | `/meters` | 查询计量点 | projectId, energyType(可选) | ApiResponse\<List\<EnergyMeter\>\> | OPS-036 |
| GET | `/meters/{id}` | 获取计量点详情 | - | ApiResponse\<EnergyMeter\> | OPS-036 |
| PUT | `/meters/{id}` | 更新计量点 | EnergyMeter | ApiResponse\<EnergyMeter\> | OPS-036 |
| DELETE | `/meters/{id}` | 删除计量点 | - | ApiResponse\<Void\> | OPS-036 |

#### 能耗记录

| 方法 | 路径 | 说明 | 请求体 | 响应 | 功能ID |
|------|------|------|--------|------|--------|
| POST | `/consumption` | 抄表记录 | RecordConsumptionRequest | ApiResponse\<EnergyConsumption\> | OPS-037 |
| GET | `/consumption/{meterId}` | 获取能耗记录 | startDate, endDate(可选) | ApiResponse\<List\<EnergyConsumption\>\> | OPS-037 |

#### 能耗统计

| 方法 | 路径 | 说明 | 参数 | 响应 | 功能ID |
|------|------|------|------|------|--------|
| GET | `/statistics/by-type` | 按类型统计 | projectId, month | ApiResponse\<Map\<EnergyType, BigDecimal\>\> | OPS-038 |
| GET | `/statistics/unit-consumption` | 单位面积能耗 | projectId, month | ApiResponse\<BigDecimal\> | OPS-039 |

**请求体定义**:

```java
// 抄表请求
RecordConsumptionRequest {
    @NotNull UUID meterId;
    @NotNull BigDecimal currentReading;
    UUID recordedBy;
}
```

---

## 四、业务规则

### 4.1 工单状态机

#### 完整状态流转图

```mermaid
stateDiagram-v2
    [*] --> PENDING : 创建工单

    PENDING --> ASSIGNED : 派单(assign)
    PENDING --> CANCELLED : 取消(cancel)

    ASSIGNED --> IN_PROGRESS : 开始执行(start)
    ASSIGNED --> SUSPENDED : 挂起(suspend)
    ASSIGNED --> RETURNED : 退回(return)
    ASSIGNED --> CANCELLED : 取消(cancel)

    IN_PROGRESS --> COMPLETED : 完成(complete)
    IN_PROGRESS --> SUSPENDED : 挂起(suspend)
    IN_PROGRESS --> CANCELLED : 取消(cancel)

    SUSPENDED --> IN_PROGRESS : 恢复(resume)\n[默认回到IN_PROGRESS]
    SUSPENDED --> ASSIGNED : 恢复(resume)\n[previousStatus=ASSIGNED时]

    RETURNED --> PENDING : 退回后重新变为待分配

    COMPLETED --> VERIFIED : 验收(verify)

    VERIFIED --> [*]
    CANCELLED --> [*]
```

#### 8种状态说明

| 状态 | 中文 | 说明 | 可转入状态 |
|------|------|------|-----------|
| PENDING | 待分配 | 工单创建后的初始状态 | ASSIGNED, CANCELLED |
| ASSIGNED | 已派单 | 已分配负责人/服务商 | IN_PROGRESS, SUSPENDED, RETURNED, CANCELLED |
| IN_PROGRESS | 执行中 | 正在处理 | COMPLETED, SUSPENDED, CANCELLED |
| SUSPENDED | 已挂起 | 暂停处理，记录前一状态 | IN_PROGRESS, ASSIGNED（取决于previousStatus） |
| RETURNED | 已退回 | 被退回重新分配 | PENDING |
| COMPLETED | 已完成 | 处理完毕 | VERIFIED |
| VERIFIED | 已验收 | 验收通过 | 终态 |
| CANCELLED | 已取消 | 工单取消 | 终态 |

#### 每个流转的前置条件与自动处理

| 当前状态 | 操作 | 目标状态 | 前置条件 | 自动处理 |
|---------|------|---------|---------|---------|
| PENDING | assign | ASSIGNED | - | 设置 assignedTo/assignedVendor/assignedDate |
| ASSIGNED | start | IN_PROGRESS | - | 设置 actualStart = now() |
| IN_PROGRESS | complete | COMPLETED | - | 设置 actualEnd = now()，自动计算 actualHours |
| COMPLETED | verify | VERIFIED | - | 设置 verifiedBy/verifiedDate/rating |
| PENDING/ASSIGNED/IN_PROGRESS | cancel | CANCELLED | status 非 COMPLETED/VERIFIED | - |
| ASSIGNED/IN_PROGRESS | suspend | SUSPENDED | - | 保存 previousStatus = 当前状态 |
| SUSPENDED | resume | previousStatus | status == SUSPENDED | 恢复到 previousStatus，默认 IN_PROGRESS |
| ASSIGNED | return | PENDING | status == ASSIGNED | 清空 assignedTo/assignedVendor/assignedDate |

#### previousStatus回溯机制

```
挂起时:
  workOrder.previousStatus = workOrder.status  // 保存当前状态
  workOrder.status = SUSPENDED

恢复时:
  targetStatus = workOrder.previousStatus != null
      ? workOrder.previousStatus
      : IN_PROGRESS  // 默认恢复到IN_PROGRESS
  workOrder.previousStatus = null  // 清空
  workOrder.status = targetStatus
```

#### 工单编号生成规则

```
格式: WO-YYYYMMDD-XXXX
示例: WO-20260518-0001

生成算法:
1. 日期前缀: "WO-" + LocalDate.now().format("yyyyMMdd") + "-"
2. 查询当天最大编号: findMaxWorkNoByPrefix(prefix + "%")
3. 序号递增: maxWorkNo存在时取后4位+1，否则从1开始
4. 格式化: String.format("%04d", sequence)
```

#### 完成工单自动计算工时

```java
actualHours = Duration.between(actualStart, actualEnd).toMinutes() / 60.0
// 精度：保留2位小数，四舍五入
```

### 4.2 维保任务触发机制

#### 4种触发类型

| 触发类型 | 枚举值 | 说明 | 优先级自动判定 |
|---------|--------|------|---------------|
| 计划触发 | PLAN | 由维保计划周期性触发 | MEDIUM |
| 巡检触发 | INSPECTION | 巡检发现异常时触发 | HIGH |
| 故障触发 | FAULT | 设备故障时触发 | HIGH（含紧急关键词时URGENT） |
| 手动创建 | MANUAL | 人工手动创建 | MEDIUM |

#### 自动优先级判定算法

```
输入: MaintenanceTask (taskType, triggerType, title, description)

1. IF taskType == EMERGENCY
   → return URGENT

2. IF triggerType == FAULT
   → 检查 title + description 是否包含紧急关键词
   → 紧急关键词: [困人, 漏水, 停电, 火灾, 爆炸, 漏电, 冒烟, 故障停机]
   → IF 包含任一关键词 → return URGENT
   → ELSE → return HIGH

3. IF triggerType == INSPECTION
   → return HIGH

4. IF triggerType == PLAN
   → return MEDIUM

5. 默认 → return MEDIUM
```

#### 维保任务状态机

```mermaid
stateDiagram-v2
    [*] --> PENDING : 创建任务

    PENDING --> ASSIGNED : 分配(assign)
    PENDING --> CANCELLED : 取消(cancel)

    ASSIGNED --> IN_PROGRESS : 开始执行(start)
    ASSIGNED --> CANCELLED : 取消(cancel)

    IN_PROGRESS --> COMPLETED : 完成(complete/complete-details)
    IN_PROGRESS --> CANCELLED : 取消(cancel)

    COMPLETED --> VERIFIED : 验收(verify)

    VERIFIED --> [*]
    CANCELLED --> [*]
```

#### 维保任务与工单状态机差异

| 差异点 | WorkOrder | MaintenanceTask |
|--------|-----------|-----------------|
| 状态数量 | 8种（含SUSPENDED/RETURNED） | 6种（无SUSPENDED/RETURNED） |
| 完成方式 | 仅一种 complete | 两种：complete（简版）+ complete-details（详版） |
| 评分方式 | 验收时评分 | 验收时评分 + 独立 rate 接口 |
| 删除行为 | 逻辑删除（isDeleted=true） | 逻辑删除（设为CANCELLED） |
| 完成后联动 | 无 | 自动更新设备维保记录 |
| 挂起/恢复 | 支持 | 不支持 |
| 退回 | 支持（ASSIGNED→PENDING） | 不支持 |

#### 维保任务完成后联动

维保任务完成（completeTaskWithDetails）时自动更新设备信息：

```
1. 更新设备维保商:
   equipment.maintenanceVendor = task.assignedVendor

2. 更新下次巡检日期（仅预防性维护）:
   IF task.taskType == PREVENTIVE
   THEN equipment.nextInspectionDate = now() + equipment.inspectionCycle
   默认周期: 30天

3. 异常容错:
   更新设备失败不影响工单完成（catch异常仅打印日志）
```

#### 任务编号生成规则

```
格式: EQ-YYYYMMDD-XXXX
示例: EQ-20260518-0001

生成算法: 与工单编号类似，前缀为 EQ-
```

### 4.3 巡检模板管理

**模板创建规则**:
- templateCode 自动生成（UNIQUE约束）
- 模板创建时可同时配置检查项列表（@Transient字段，需单独持久化）
- 模板状态默认为 ACTIVE

**模板复制规则**:
- 复制模板时创建新模板，名称使用传入的 newName
- 检查项同时复制到新模板
- 新模板的 templateCode 自动生成

**检查项配置**:
- InspectionItem 通过 templateId 关联模板
- isMandatory = true 的检查项为必检项，不可跳过
- isNormalRequired = true 的检查项需要判定正常/异常
- sortOrder 控制检查项的展示顺序

### 4.4 备件库存管理

#### 入库流程

```
1. 校验: quantity > 0
2. 查询备件: getSparePartById(sparePartId)
3. 更新库存: currentStock += quantity
4. 创建入库记录: RecordType=IN, balance=更新后库存
5. 生成记录编码: REC + IN + yyyyMMddHHmmss
```

#### 出库流程

```
1. 校验: quantity > 0
2. 查询备件: getSparePartById(sparePartId)
3. 校验库存: currentStock >= quantity
   → 不满足: 抛出 BusinessException("库存不足，当前库存：X，需要出库：Y")
4. 更新库存: currentStock -= quantity
5. 创建出库记录: RecordType=OUT, balance=更新后库存, relatedOrderId=关联工单
6. 生成记录编码: REC + OUT + yyyyMMddHHmmss
```

#### 低库存预警

```
预警条件: currentStock < safeStock
查询方式: sparePartRepository.findLowStockParts(projectId)
```

#### 库存余额计算

```
每次出入库操作后:
  record.balance = sparePart.currentStock  (操作后的库存余额)
```

#### 关联工单

- 出库时可指定 relatedOrderId 关联工单
- 用于追踪工单的备件消耗

#### 并发场景考虑

> 当前实现未使用乐观锁或悲观锁，存在并发安全问题：

| 并发场景 | 风险 | 当前处理 | 建议方案 |
|----------|------|----------|---------|
| 同时出库 | 库存超卖 | 无保护 | 添加 @Version 乐观锁或 SELECT FOR UPDATE 悲观锁 |
| 同时入库 | 库存计算错误 | 无保护 | 添加 @Version 乐观锁 |
| 并发读写 | 读到中间状态 | 无保护 | 使用数据库事务隔离级别 |

**建议并发控制方案**:

```java
// 方案1: 乐观锁（推荐）
@Version
private Long version;

// 出库时:
SparePart sp = repository.findById(id);
if (sp.getCurrentStock() < quantity) throw ...;
sp.setCurrentStock(sp.getCurrentStock() - quantity);
repository.save(sp);  // version不匹配时抛出OptimisticLockException

// 方案2: 悲观锁
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT sp FROM SparePart sp WHERE sp.id = :id")
SparePart findByIdForUpdate(@Param("id") UUID id);
```

### 4.5 能耗计量管理

#### 抄表流程

```
1. 校验计量点: meterId 存在且 status == ACTIVE
2. 获取上次抄表记录: findTopByMeterIdOrderByConsumptionDateDesc
3. 计算消耗量: consumption = currentReading - previousReading
4. 校验读数: consumption >= 0（当前读数不能小于上次读数）
5. 计算费用: amount = consumption x meter.unitPrice
6. 创建能耗记录: recordMethod=MANUAL
```

#### 消耗量计算

```
consumption = currentReading - previousReading

特殊情况:
- 首次抄表: previousReading = 0
- 读数倒退: 抛出 BusinessException("当前读数不能小于上次读数")
```

#### 费用计算

```
amount = consumption x meter.unitPrice

条件:
- meter.unitPrice 不为空
- consumption > 0
```

#### 按类型统计

```
输入: projectId, month (LocalDate)
1. 计算月份范围: startDate = month.withDayOfMonth(1), endDate = month.withDayOfMonth(length)
2. 初始化所有EnergyType为0
3. 查询项目总消耗: sumConsumptionByProjectAndDateRange
4. 当前实现: 将总消耗分配给LIGHTING类型（TODO: 应按meter.energyType汇总）
```

> 当前按类型统计实现不完整，所有消耗归入LIGHTING类型，需改为按meter.energyType分别汇总。

#### 单位面积能耗

```
输入: projectId, month
1. 获取项目所有ACTIVE状态的计量点
2. 遍历每个计量点，查询当月消耗量
3. 累加总消耗量

注意: 当前实现未除以建筑面积，仅返回总消耗量
TODO: 需要获取项目建筑面积，计算 单位面积能耗 = 总消耗量 / 建筑面积
```

### 4.6 工单明细管理

#### 3种明细类型

| 类型 | 枚举值 | 中文 | 用途 |
|------|--------|------|------|
| PART | 备件 | 记录工单使用的备件及费用 |
| INSPECTION_ITEM | 巡检项 | 记录工单的巡检检查项 |
| CHECKPOINT | 检查点 | 记录工单的关键检查点 |

#### 费用计算

```
前端计算:
  item.totalPrice = item.quantity x item.unitPrice
  partsCost = SUM(item.totalPrice)  WHERE item.itemType == PART
  totalCost = laborCost + partsCost
```

#### 批量添加

- 支持批量添加工单明细
- 受 BatchOperationValidator 限制（校验批量操作大小）
- 每条明细的 workOrderId 自动设置为当前工单ID
- sortOrder 控制明细展示顺序

---

## 五、执行约束

### 5.1 工单编号唯一性

- workNo 字段在数据库层设置 UNIQUE 约束
- 生成算法基于日期前缀+4位序号，按日期递增
- 编号格式: `WO-YYYYMMDD-XXXX`
- 同一天内序号从0001开始递增

### 5.2 任务编号唯一性

- taskNo 字段在数据库层设置 UNIQUE 约束
- 编号格式: `EQ-YYYYMMDD-XXXX`
- 生成算法与工单编号类似

### 5.3 备件库存非负约束

- 出库时校验: `currentStock >= quantity`
- 不满足时抛出 BusinessException(6104, "库存不足，当前库存：X，需要出库：Y")
- 入库数量必须 > 0，否则抛出 BusinessException(6102, "入库数量必须大于0")
- 出库数量必须 > 0，否则抛出 BusinessException(6103, "出库数量必须大于0")

### 5.4 能耗读数递增约束

- 抄表时校验: `currentReading >= previousReading`
- 不满足时抛出 BusinessException(6103, "当前读数不能小于上次读数")
- 首次抄表时 previousReading = 0

### 5.5 工单状态流转约束

- 只能按合法路径流转，非法流转抛出 RuntimeException
- COMPLETED/VERIFIED 状态不可取消
- SUSPENDED 状态只能恢复（resume），不能直接转为其他状态
- RETURNED 状态自动转为 PENDING

**合法流转矩阵**:

| 当前\目标 | PENDING | ASSIGNED | IN_PROGRESS | SUSPENDED | RETURNED | COMPLETED | VERIFIED | CANCELLED |
|----------|---------|----------|-------------|-----------|----------|-----------|----------|-----------|
| PENDING | - | V | - | - | - | - | - | V |
| ASSIGNED | - | - | V | V | V | - | - | V |
| IN_PROGRESS | - | - | - | V | - | V | - | V |
| SUSPENDED | - | V* | V* | - | - | - | - | - |
| RETURNED | V | - | - | - | - | - | - | - |
| COMPLETED | - | - | - | - | - | - | V | - |
| VERIFIED | - | - | - | - | - | - | - | - |
| CANCELLED | - | - | - | - | - | - | - | - |

> V* 表示 SUSPENDED 恢复时回到 previousStatus

### 5.6 维保计划周期约束

- cycleDays 必须 > 0（周期天数必须为正数）
- nextDate = lastDate + cycleDays（下次执行日期由周期推算）
- 计划状态变更：ACTIVE 可转为 INACTIVE 或 SUSPENDED

---

## 六、权限控制

| API路径 | 操作 | 所需权限 | 说明 |
|---------|------|----------|------|
| POST /api/wo/work-orders | 创建工单 | wo:work-order:create | 需项目级权限 |
| GET /api/wo/work-orders | 查询工单 | wo:work-order:read | 需项目级权限 |
| PUT /api/wo/work-orders/{id} | 更新工单 | wo:work-order:update | 需项目级权限 |
| DELETE /api/wo/work-orders/{id} | 删除工单 | wo:work-order:delete | 需项目级权限 |
| POST /api/wo/work-orders/{id}/assign | 派单 | wo:work-order:assign | 需项目级权限 |
| POST /api/wo/work-orders/{id}/start | 开始执行 | wo:work-order:execute | 需项目级权限 |
| POST /api/wo/work-orders/{id}/complete | 完成工单 | wo:work-order:execute | 需项目级权限 |
| POST /api/wo/work-orders/{id}/verify | 验收工单 | wo:work-order:verify | 需项目级权限 |
| POST /api/wo/work-orders/{id}/cancel | 取消工单 | wo:work-order:cancel | 需项目级权限 |
| POST /api/wo/work-orders/{id}/suspend | 挂起工单 | wo:work-order:suspend | 需项目级权限 |
| POST /api/wo/work-orders/{id}/resume | 恢复工单 | wo:work-order:resume | 需项目级权限 |
| POST /api/wo/work-orders/{id}/return | 退回工单 | wo:work-order:return | 需项目级权限 |
| POST /api/ops/maintenance-tasks | 创建维保任务 | ops:maintenance-task:create | 需项目级权限 |
| POST /api/ops/maintenance-tasks/{id}/assign | 分配任务 | ops:maintenance-task:assign | 需项目级权限 |
| POST /api/ops/maintenance-tasks/{id}/start | 开始执行 | ops:maintenance-task:execute | 需项目级权限 |
| POST /api/ops/maintenance-tasks/{id}/complete | 完成任务 | ops:maintenance-task:execute | 需项目级权限 |
| POST /api/ops/maintenance-tasks/{id}/verify | 验收任务 | ops:maintenance-task:verify | 需项目级权限 |
| POST /api/ops/spare-parts/in-stock | 入库 | ops:spare-part:stock-in | 需项目级权限 |
| POST /api/ops/spare-parts/out-stock | 出库 | ops:spare-part:stock-out | 需项目级权限 |
| POST /api/ops/energy/consumption | 抄表 | ops:energy:record | 需项目级权限 |

> 当前实现中权限控制尚未集成Spring Security，以上为设计预期权限矩阵。

---

## 七、例外情况处理

| 例外场景 | 触发条件 | 处理方式 | 错误码/信息 |
|----------|----------|----------|------------|
| 工单状态流转非法 | 从PENDING直接complete | 抛出RuntimeException | "只能完成进行中的工单" |
| 工单已完成后取消 | status=COMPLETED/VERIFIED时cancel | 抛出RuntimeException | "无法取消已完成的工单" |
| 挂起非ASSIGNED/IN_PROGRESS工单 | status不合法时suspend | 抛出RuntimeException | "只能挂起已派单或执行中的工单" |
| 恢复非SUSPENDED工单 | status!=SUSPENDED时resume | 抛出RuntimeException | "只能恢复已挂起的工单" |
| 退回非ASSIGNED工单 | status!=ASSIGNED时return | 抛出RuntimeException | "只能退回已派单的工单" |
| 备件库存不足 | 出库数量>当前库存 | 抛出BusinessException | 6104 "库存不足，当前库存：X，需要出库：Y" |
| 入库数量非法 | quantity <= 0 | 抛出BusinessException | 6102 "入库数量必须大于0" |
| 出库数量非法 | quantity <= 0 | 抛出BusinessException | 6103 "出库数量必须大于0" |
| 能耗读数倒退 | currentReading < previousReading | 抛出BusinessException | 6103 "当前读数不能小于上次读数" |
| 能源仪表不存在 | meterId无效 | 抛出BusinessException | 6101 "能源仪表不存在" |
| 仪表非ACTIVE状态 | 抄表时meter.status!=ACTIVE | 抛出BusinessException | 6102 "只能对ACTIVE状态的仪表进行抄表" |
| 维保计划关联设备不存在 | equipmentId无效 | 数据库外键约束 | 需添加业务校验 |
| 维保任务不存在 | taskId无效 | 抛出BusinessException | ErrorCode.NOT_FOUND "维保工单不存在" |
| 工单不存在 | workOrderId无效或已删除 | 抛出RuntimeException | "工单不存在: {id}" |
| 维保任务评分越界 | rating < 1 或 rating > 5 | 抛出BusinessException | 6006 "评分必须在1-5之间" |
| 维保任务评分状态非法 | 非COMPLETED/VERIFIED状态评分 | 抛出BusinessException | 6005 "只有已完成或已验收的工单才能评分" |
| 巡检模板被引用不能删除 | 模板被巡检记录引用 | - | 当前无校验，建议增加 |
| 备件不存在 | sparePartId无效 | 抛出BusinessException | 6101 "备件不存在" |

---

**附录：已知代码问题与改进建议**

| 问题 | 位置 | 影响 | 建议 |
|------|------|------|------|
| 备件出入库无并发控制 | SparePartServiceImpl | 高并发下可能库存超卖 | 添加 @Version 乐观锁或 SELECT FOR UPDATE |
| 能耗按类型统计实现不完整 | EnergyConsumptionServiceImpl | 所有消耗归入LIGHTING类型 | 按 meter.energyType 分别汇总 |
| 单位面积能耗未除以建筑面积 | EnergyConsumptionServiceImpl | 返回的是总消耗量而非单位面积能耗 | 获取项目建筑面积做除法 |
| 工单查询参数互斥 | WorkOrderController/MaintenanceTaskController | 多条件同时传入只生效一个 | 改为 Specification 动态查询 |
| 维保任务查询无分页 | MaintenanceTaskController | 数据量大时可能OOM | 改为分页查询 |
| 工单删除/退回使用RuntimeException | WorkOrderServiceImpl | 异常信息不够结构化 | 改为 BusinessException + ErrorCode |
| 维保任务删除实际是取消 | MaintenanceTaskServiceImpl | deleteTask 实际设置 status=CANCELLED | 命名与行为不一致，建议重命名 |
| 前后端状态枚举不一致 | maintenance.ts vs MaintenanceTask | 前端含ACCEPTED，后端含ASSIGNED/VERIFIED | 统一前后端状态定义 |
| 前端维保计划两套API | maintenance.ts + maintenance-plan.ts | 接口重复，维护困难 | 统一为一套API |
| 工单无流转记录 | WorkOrderServiceImpl | 无法追溯工单状态变更历史 | 新增 WorkOrderFlow 实体 |
