ether-docs/02-DESIGN/detail/DETAIL-OPS.md

1363 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 运营与服务域 - 详细设计
**文档编号**: 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-XXXXUNIQUE |
| 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-XXXXUNIQUE |
| 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 实体 |