# 空间与项目域 - 详细设计

**文档版本**: v1.0
**生成日期**: 2026-05-18
**数据来源**: module-mdm 实际代码 + REVERSE-MDM.md
**对照需求**: 02-SPACE_NODE_DESIGN.md

---

## 一、功能点清单

| 功能ID | 功能名称 | 优先级 | 实现状态 | 对应需求ID |
|--------|----------|--------|----------|------------|
| SPACE-001 | 项目CRUD | P0 | 已实现 | 02-SPACE-001 |
| SPACE-002 | 项目编码自动生成 | P0 | 已实现 | 02-SPACE-002 |
| SPACE-003 | 项目状态流转 | P0 | 已实现 | 02-SPACE-003 |
| SPACE-004 | 项目成员管理 | P0 | 已实现 | 02-SPACE-004 |
| SPACE-005 | 项目统计数据 | P1 | 已实现 | 02-SPACE-005 |
| SPACE-006 | 项目配置管理 | P1 | 已实现 | 02-SPACE-006 |
| SPACE-007 | 项目删除前检查 | P0 | 已实现 | 02-SPACE-007 |
| SPACE-008 | 项目选择器列表 | P1 | 已实现 | 02-SPACE-008 |
| SPACE-009 | 空间节点CRUD | P0 | 已实现 | 02-SPACE-010 |
| SPACE-010 | 空间节点树形查询 | P0 | 已实现 | 02-SPACE-011 |
| SPACE-011 | 空间节点批量创建 | P1 | 已实现 | 02-SPACE-012 |
| SPACE-012 | 空间节点删除检查 | P0 | 已实现 | 02-SPACE-013 |
| SPACE-013 | 空间节点级联删除 | P0 | 已实现 | 02-SPACE-014 |
| SPACE-014 | 设备管理(CRUD) | P1 | 已实现(@Deprecated) | 02-SPACE-020 |
| SPACE-015 | 设备批量创建 | P2 | 已实现(@Deprecated) | 02-SPACE-021 |
| SPACE-016 | 设备Excel导入 | P2 | 已实现(@Deprecated) | 02-SPACE-022 |
| SPACE-017 | 特种设备查询 | P2 | 已实现(@Deprecated) | 02-SPACE-023 |
| SPACE-018 | 即将年检设备查询 | P2 | 已实现(@Deprecated) | 02-SPACE-024 |
| SPACE-019 | 楼栋楼层信息 | P1 | 已实现 | 02-SPACE-030 |
| SPACE-020 | 巡检标准项CRUD | P0 | 已实现 | 02-SPACE-040 |
| SPACE-021 | 巡检模板管理 | P1 | 已实现 | 02-SPACE-041 |
| SPACE-022 | 巡检记录CRUD | P0 | 已实现 | 02-SPACE-042 |
| SPACE-023 | 巡检完成确认 | P1 | 已实现 | 02-SPACE-043 |
| SPACE-024 | 备件CRUD | P1 | 已实现 | 02-SPACE-050 |
| SPACE-025 | 备件出入库 | P0 | 已实现 | 02-SPACE-051 |
| SPACE-026 | 备件低库存预警 | P1 | 已实现 | 02-SPACE-052 |
| SPACE-027 | 备件分类管理 | P1 | 已实现 | 02-SPACE-053 |
| SPACE-028 | 计量点CRUD | P1 | 已实现 | 02-SPACE-060 |
| SPACE-029 | 能耗记录录入 | P1 | 已实现 | 02-SPACE-061 |
| SPACE-030 | 能耗统计 | P2 | 已实现 | 02-SPACE-062 |

---

## 二、数据结构设计

### 2.1 实体定义

#### 2.1.1 Project（项目）

**表名**: `mdm_project`

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | UUID | PK, 自动生成 | 项目唯一标识符 |
| code | VARCHAR(50) | NOT NULL, UNIQUE, 正则`^[a-zA-Z0-9_-]+$`, 2-50位 | 项目编码 |
| name | VARCHAR(100) | NOT NULL, 2-100位 | 项目名称 |
| description | VARCHAR(500) | | 项目描述 |
| address | VARCHAR(100) | | 项目地址 |
| projectType | VARCHAR(20) | @Enumerated(STRING), 默认RESIDENTIAL | 项目类型枚举 |
| province | VARCHAR(50) | | 省 |
| city | VARCHAR(50) | | 市 |
| district | VARCHAR(50) | | 区 |
| longitude | Double | | 经度 |
| latitude | Double | | 纬度 |
| status | VARCHAR(20) | NOT NULL, 默认ACTIVE | 项目状态 |
| buildingCount | Integer | | 楼栋数 |
| unitCount | Integer | | 单元数 |
| roomCount | Integer | | 房间数 |
| floorCount | Integer | | 楼层数 |
| logo | VARCHAR(200) | | 项目Logo |
| contact | VARCHAR(200) | | 联系人 |
| contactPhone | VARCHAR(20) | 正则`^1[3-9]\d{9}$` | 联系电话 |
| createdAt | LocalDateTime | NOT NULL, @PrePersist自动填充 | 创建时间 |
| updatedAt | LocalDateTime | NOT NULL, @PreUpdate自动填充 | 更新时间 |

**枚举: ProjectType**

| 值 | 描述 |
|----|------|
| RESIDENTIAL | 住宅 |
| OFFICE | 办公 |
| INDUSTRIAL_PARK | 产业园区 |

**项目状态值**（String类型，非枚举）:

| 值 | 描述 |
|----|------|
| ACTIVE | 正常 |
| DISABLED | 禁用 |
| PENDING | 待审核 |
| ARCHIVED | 已归档 |

**索引**:
- `mdm_project_code_key` → code (UNIQUE)

---

#### 2.1.2 SpaceNode（空间节点）-- 核心实体

**表名**: `mdm_space_node`

##### 基础字段

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | UUID | PK, 自动生成 | 节点唯一标识符 |
| projectId | UUID | NOT NULL, column: project_code | 项目ID（注意列名为project_code） |
| code | VARCHAR(50) | @JsonIgnore | 空间编码（保留字段，暂未使用） |
| name | VARCHAR(100) | NOT NULL, 1-100位 | 节点名称 |
| fullName | VARCHAR(500) | | 全路径名称 |
| shortName | VARCHAR(50) | | 简称 |
| nodeCategory | VARCHAR(20) | NOT NULL, @Enumerated(STRING), column: node_category | 节点大类枚举 |
| nodeType | VARCHAR(30) | NOT NULL, @Enumerated(STRING), column: node_type | 节点类型枚举 |
| usageType | VARCHAR(30) | | 用途类型 |
| parentId | UUID | column: parent_id | 父节点ID |
| treePath | VARCHAR(1000) | column: tree_path | 物理路径 id.id.id |
| treePathName | VARCHAR(1000) | column: tree_path_name | 名称路径 项目/楼栋/单元/房间 |
| level | Integer | 默认0 | 层级深度 |
| sortOrder | Integer | column: sort_order, 默认0 | 排序号 |
| status | VARCHAR(20) | 默认ACTIVE | 状态 |
| deliveryStatus | VARCHAR(20) | column: delivery_status | 交付状态 |
| decorationStatus | VARCHAR(20) | column: decoration_status | 装修状态 |

##### 面积信息

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| buildingArea | BigDecimal(10,2) | column: building_area | 建筑面积(㎡) |
| usableArea | BigDecimal(10,2) | column: usable_area | 使用面积(㎡) |
| sharedArea | BigDecimal(10,2) | column: shared_area | 公摊面积(㎡) |
| landArea | BigDecimal(10,2) | column: land_area | 占地面积(㎡) |

##### 地理信息

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| longitude | BigDecimal(10) | | 经度 |
| latitude | BigDecimal(10) | | 纬度 |
| altitude | BigDecimal(8,2) | | 海拔 |
| floorNumber | Integer | column: floor_number | 楼层号(正数地上,负数地下) |

##### 地址信息

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| province | VARCHAR(50) | | 省 |
| city | VARCHAR(50) | | 市 |
| district | VARCHAR(50) | | 区 |
| street | VARCHAR(100) | | 街道 |
| address | VARCHAR(255) | | 详细地址 |

##### 扩展属性

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| attributes | VARCHAR(2000) | | 类型特定属性(JSON格式) |

##### 系统字段

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| createdAt | LocalDateTime | column: created_at | 创建时间 |
| updatedAt | LocalDateTime | column: updated_at | 更新时间 |
| createdBy | UUID | column: created_by | 创建人 |
| updatedBy | UUID | column: updated_by | 更新人 |
| isDeleted | Boolean | column: is_deleted, 默认false | 软删除标记 |

##### 设备扩展字段（@Deprecated -- 请使用 module-asset 的 Equipment 实体）

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| isEquipment | Boolean | @Deprecated, column: is_equipment, 默认false | 是否为设备节点 |
| designLifeYears | Integer | @Deprecated, column: design_life_years | 设计寿命(年) |
| ratedPower | BigDecimal(10,2) | @Deprecated, column: rated_power | 额定功率 |
| ratedVoltage | BigDecimal(10,2) | @Deprecated, column: rated_voltage | 额定电压 |
| ratedCurrent | BigDecimal(10,2) | @Deprecated, column: rated_current | 额定电流 |
| maintenanceVendor | VARCHAR(100) | @Deprecated, column: maintenance_vendor | 维保厂商 |
| maintenanceVendorContact | VARCHAR(50) | @Deprecated, column: maintenance_vendor_contact | 维保联系人 |
| maintenanceVendorPhone | VARCHAR(20) | @Deprecated, column: maintenance_vendor_phone | 维保电话 |
| maintenanceContractNo | VARCHAR(50) | @Deprecated, column: maintenance_contract_no | 维保合同号 |
| maintenanceContractStart | LocalDate | @Deprecated, column: maintenance_contract_start | 合同开始日期 |
| maintenanceContractEnd | LocalDate | @Deprecated, column: maintenance_contract_end | 合同结束日期 |
| specialEquipmentType | VARCHAR(50) | @Deprecated, column: special_equipment_type | 特种设备类型 |
| specialEquipmentCert | VARCHAR(100) | @Deprecated, column: special_equipment_cert | 特种设备证书 |
| inspectionCycle | Integer | @Deprecated, column: inspection_cycle | 巡检周期(天) |
| nextInspectionDate | LocalDate | @Deprecated, column: next_inspection_date | 下次年检日期 |
| lastInspectionDate | LocalDate | @Deprecated, column: last_inspection_date | 上次年检日期 |
| lastInspectionResult | VARCHAR(20) | @Deprecated, column: last_inspection_result | 上次年检结果 |
| commonSpareParts | VARCHAR(2000) | @Deprecated, column: common_spare_parts | 常用备件(JSON) |
| energyConsumptionStandard | BigDecimal(12,2) | @Deprecated, column: energy_consumption_standard | 能耗标准 |
| installationEnvironment | VARCHAR(50) | @Deprecated, column: installation_environment | 安装环境 |
| protectionLevel | VARCHAR(20) | @Deprecated, column: protection_level | 防护等级 |

**索引**:

| 索引名 | 列 | 说明 |
|--------|-----|------|
| idx_space_node_project | project_id | 按项目查询 |
| idx_space_node_parent | parent_id | 查子节点 |
| idx_space_node_type | node_type | 按类型查询 |
| idx_space_node_tree_path | tree_path | 路径查询 |
| idx_sn_project_parent | project_id, parent_id | 项目+父节点 |
| idx_sn_project_type | project_id, node_type | 项目+类型 |
| idx_sn_project_isequipment | project_id, is_equipment | 项目+设备 |
| idx_sn_project_nextinspection | project_id, next_inspection_date | 年检预警 |

---

#### 2.1.3 ProjectConfig（项目配置）

**表名**: `mdm_project_config`

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | UUID | PK, 自动生成 | 主键 |
| projectId | UUID | NOT NULL, UNIQUE, column: project_id | 项目ID |
| enableReservation | Boolean | column: enable_reservation, 默认false | 预约功能 |
| enableVisitor | Boolean | column: enable_visitor, 默认false | 访客功能 |
| enableComplaint | Boolean | column: enable_complaint, 默认true | 投诉功能 |
| enablePayment | Boolean | column: enable_payment, 默认false | 缴费功能 |
| enableAnnouncement | Boolean | column: enable_announcement, 默认true | 公告功能 |
| enableSurvey | Boolean | column: enable_survey, 默认false | 问卷功能 |
| enableVote | Boolean | column: enable_vote, 默认false | 投票功能 |
| enableMaintenance | Boolean | column: enable_maintenance, 默认true | 报修功能 |
| enableAsset | Boolean | column: enable_asset, 默认false | 资产功能 |
| customConfig | VARCHAR(5000) | column: custom_config | 自定义配置JSON |
| createdAt | LocalDateTime | column: created_at | 创建时间 |
| updatedAt | LocalDateTime | column: updated_at | 更新时间 |

**索引**:
- `mdm_project_config_project_id_key` → projectId (UNIQUE)

---

#### 2.1.4 ProjectStatistics（项目统计）

**表名**: `mdm_project_statistics`

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | UUID | PK, 自动生成 | 主键 |
| projectId | UUID | NOT NULL, UNIQUE, column: project_id | 项目ID |
| memberCount | Integer | column: member_count, 默认0 | 成员数 |
| buildingCount | Integer | column: building_count, 默认0 | 楼栋数 |
| unitCount | Integer | column: unit_count, 默认0 | 单元数 |
| roomCount | Integer | column: room_count, 默认0 | 房间数 |
| ownerCount | Integer | column: owner_count, 默认0 | 业主数 |
| tenantCount | Integer | column: tenant_count, 默认0 | 租户数 |
| lastSyncedAt | LocalDateTime | column: last_synced_at | 最后同步时间 |
| createdAt | LocalDateTime | column: created_at | 创建时间 |
| updatedAt | LocalDateTime | column: updated_at | 更新时间 |

**索引**:
- `mdm_project_statistics_project_id_key` → projectId (UNIQUE)

---

#### 2.1.5 ProjectStatusHistory（状态变更历史）

**表名**: `mdm_project_status_history`

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | UUID | PK, 自动生成 | 主键 |
| projectId | UUID | NOT NULL, column: project_id | 项目ID |
| fromStatus | VARCHAR(20) | column: from_status | 原状态 |
| toStatus | VARCHAR(20) | NOT NULL, column: to_status | 新状态 |
| reason | VARCHAR(500) | | 变更原因 |
| operatorId | UUID | column: operator_id | 操作人ID |
| operatorName | VARCHAR(50) | column: operator_name | 操作人姓名 |
| createdAt | LocalDateTime | column: created_at, @PrePersist自动填充 | 创建时间 |

---

#### 2.1.6 InspectionItem（巡检标准项）

**表名**: `mdm_inspection_item`

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | UUID | PK, 自动生成 | 主键 |
| equipmentType | VARCHAR(50) | column: equipment_type | 设备类型 |
| systemType | VARCHAR(50) | column: system_type | 系统类型 |
| itemName | VARCHAR(200) | NOT NULL, column: item_name | 检查项名称 |
| checkMethod | VARCHAR(200) | column: check_method | 检查方法 |
| standardValue | VARCHAR(100) | column: standard_value | 标准值 |
| isRequired | Boolean | column: is_required, 默认true | 是否必检 |
| remark | VARCHAR(500) | | 备注 |
| sortOrder | Integer | column: sort_order | 排序号 |
| status | VARCHAR(20) | NOT NULL, @Enumerated(STRING), 默认ACTIVE | 状态枚举 |
| createdAt | LocalDateTime | column: created_at, @PrePersist自动填充 | 创建时间 |
| updatedAt | LocalDateTime | column: updated_at, @PreUpdate自动填充 | 更新时间 |

**枚举: Status**

| 值 | 描述 |
|----|------|
| ACTIVE | 启用 |
| INACTIVE | 停用 |

---

#### 2.1.7 InspectionRecord（巡检记录）

**表名**: `mdm_inspection_record`

| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | UUID | PK, 自动生成 | 主键 |
| planId | UUID | column: plan_id | 巡检计划ID |
| equipmentId | UUID | NOT NULL, column: equipment_id | 设备ID |
| inspectionDate | LocalDate | NOT NULL, column: inspection_date | 巡检日期 |
| inspector | VARCHAR(200) | NOT NULL | 巡检人 |
| status | VARCHAR(20) | @Enumerated(STRING), 默认NORMAL | 检查状态枚举 |
| checkInTime | LocalDateTime | column: check_in_time | 签到时间 |
| checkInLocation | VARCHAR(100) | column: check_in_location | 签到位置 |
| checkInPhoto | VARCHAR(200) | column: check_in_photo | 签到照片 |
| items | JSONB | @JdbcTypeCode(JSON), columnDefinition: jsonb | 检查项结果列表 |
| problems | JSONB | @JdbcTypeCode(JSON), columnDefinition: jsonb | 异常问题列表 |
| completed | Boolean | 默认false | 是否完成 |
| completedTime | LocalDateTime | column: completed_time | 完成时间 |
| createdAt | LocalDateTime | column: created_at, @PrePersist自动填充 | 创建时间 |

**枚举: CheckStatus**

| 值 | 描述 |
|----|------|
| NORMAL | 正常 |
| WARNING | 预警 |
| ABNORMAL | 异常 |

**items JSONB结构**:
```json
[
  {"itemId": "uuid", "itemName": "检查项", "value": "实测值", "result": "PASS/FAIL", "remark": "备注"}
]
```

**problems JSONB结构**:
```json
[
  {"desc": "问题描述", "photo": "照片URL", "severity": "LOW/MEDIUM/HIGH"}
]
```

**索引**:

| 索引名 | 列 |
|--------|-----|
| idx_ir_equipment_date | equipment_id, inspection_date |
| idx_ir_inspectiondate | inspection_date |

---

### 2.2 枚举定义

#### NodeCategory（节点大类）

| 值 | 描述 |
|----|------|
| BUILDING | 建筑空间 |
| PARKING | 停车空间 |
| FACILITY | 设施空间 |
| AREA | 区域空间 |

#### NodeType（节点类型）-- 含分类和层级

| 值 | 描述 | 所属大类 | 层级序号 |
|----|------|----------|----------|
| BUILDING | 楼栋 | BUILDING | 1 |
| UNIT | 单元 | BUILDING | 2 |
| FLOOR | 楼层 | BUILDING | 3 |
| ROOM | 房间 | BUILDING | 4 |
| SHOP | 商铺 | BUILDING | 2 |
| GARAGE | 车库 | PARKING | 1 |
| PARKING_AREA | 停车区域 | PARKING | 2 |
| PARKING_SPACE | 车位 | PARKING | 3 |
| EQUIPMENT_ROOM | 设备房 | FACILITY | 1 |
| PROPERTY_OFFICE | 物业用房 | FACILITY | 1 |
| SECURITY_ROOM | 门岗 | FACILITY | 1 |
| PUBLIC_ROOM | 公共用房 | FACILITY | 1 |
| PUBLIC_AREA | 公共区域 | AREA | 1 |
| GREEN_AREA | 绿化区域 | AREA | 1 |
| ROAD | 道路 | AREA | 1 |

#### ProjectType（项目类型）

| 值 | 描述 |
|----|------|
| RESIDENTIAL | 住宅 |
| OFFICE | 办公 |
| INDUSTRIAL_PARK | 产业园区 |

#### InspectionItem.Status（巡检标准项状态）

| 值 | 描述 |
|----|------|
| ACTIVE | 启用 |
| INACTIVE | 停用 |

#### CheckStatus（巡检检查状态）

| 值 | 描述 |
|----|------|
| NORMAL | 正常 |
| WARNING | 预警 |
| ABNORMAL | 异常 |

---

### 2.3 ER关系图（Mermaid）

```mermaid
erDiagram
    Project ||--o{ SpaceNode : "1:N projectId"
    Project ||--|| ProjectConfig : "1:1 projectId UNIQUE"
    Project ||--|| ProjectStatistics : "1:1 projectId UNIQUE"
    Project ||--o{ ProjectStatusHistory : "1:N projectId"

    SpaceNode ||--o{ SpaceNode : "自引用树形 parentId"
    SpaceNode ||--o| SpaceNode : "设备扩展 isEquipment=true"

    InspectionItem {
        UUID id PK
        String equipmentType
        String systemType
        String itemName
        String status
    }

    InspectionTemplate {
        UUID id PK
        UUID projectId
        String templateCode
        String equipmentType
        String inspectionItems
    }

    InspectionRecord {
        UUID id PK
        UUID planId
        UUID equipmentId
        LocalDate inspectionDate
        String inspector
        String status
    }

    InspectionRecord }o--|| SpaceNode : "equipmentId"

    SparePartCategory ||--o{ SparePartCategory : "自引用树形 parentId"
    SparePartCategory ||--o{ SparePart : "1:N categoryId"
    SparePart ||--o{ SparePartRecord : "1:N sparePartId"

    EnergyMeter ||--o{ EnergyConsumption : "1:N meterId"
    EnergyMeter }o--o| SpaceNode : "spaceNodeId"
```

---

## 三、API设计

### 3.1 ProjectController -- `/api/mdm/projects`

| 编号 | 方法 | 路径 | 说明 | 请求参数 | 响应格式 | 权限要求 | 例外情况 |
|------|------|------|------|----------|----------|----------|----------|
| PROJ-API-001 | GET | `/` | 分页查询项目列表 | keyword(String, 可选), status(String, 可选), page(int, 默认0), size(int, 默认10), sortBy(String, 可选), sortDirection(String, 可选) | `{code, data: PageResponse<Project>}` | 已登录 | 分页参数越界 |
| PROJ-API-002 | GET | `/selector` | 获取项目选择器列表 | 无 | `{code, data: [ProjectSelectorItem]}` | 已登录 | 无 |
| PROJ-API-003 | GET | `/generate-code` | 生成项目编码 | 无 | `{code, data: "PRJ-xxx"}` | 管理员 | 无 |
| PROJ-API-004 | GET | `/{id}` | 按ID查询项目 | id(UUID, Path) | `{code, data: Project}` | 已登录 | 项目不存在 |
| PROJ-API-005 | GET | `/code/{code}` | 按编码查询项目 | code(String, Path) | `{code, data: Project}` | 已登录 | 编码不存在 |
| PROJ-API-006 | POST | `/` | 创建项目 | Body: Project实体 | `{code, data: Project}` | 管理员 | 项目编码已存在; 字段校验失败 |
| PROJ-API-007 | PUT | `/{id}` | 更新项目 | id(UUID, Path), Body: Project实体 | `{code, data: Project}` | 管理员 | 项目不存在; 编码冲突 |
| PROJ-API-008 | DELETE | `/{id}` | 删除项目 | id(UUID, Path) | `{code, data: null}` | 管理员 | 项目不存在; 有关联数据 |
| PROJ-API-009 | GET | `/{id}/members` | 获取项目成员列表 | id(UUID, Path), page(int, 默认0), size(int, 默认20) | `{code, data: PageResponse<ProjectMemberDTO>}` | 项目成员 | 项目不存在 |
| PROJ-API-010 | POST | `/{id}/members` | 添加项目成员 | id(UUID, Path), Body: AddMemberRequest `{userIds: [UUID], roleInProject: String}` | `{code, data: null}` | 项目管理员 | 项目不存在; 用户不存在; 已是成员 |
| PROJ-API-011 | DELETE | `/{id}/members/{memberId}` | 移除项目成员 | id(UUID, Path), memberId(UUID, Path) | `{code, data: null}` | 项目管理员 | 成员不存在 |
| PROJ-API-012 | GET | `/{id}/statistics` | 获取项目统计数据 | id(UUID, Path) | `{code, data: ProjectStatistics}` | 项目成员 | 项目不存在 |
| PROJ-API-013 | PUT | `/{id}/status` | 变更项目状态 | id(UUID, Path), Body: ChangeStatusRequest `{status: String, reason?: String}` | `{code, data: null}` | 管理员 | 项目不存在; 状态流转非法 |
| PROJ-API-014 | GET | `/{id}/config` | 获取项目配置 | id(UUID, Path) | `{code, data: ProjectConfigDTO}` | 项目成员 | 项目不存在 |
| PROJ-API-015 | PUT | `/{id}/config` | 更新项目配置 | id(UUID, Path), Body: ProjectConfigDTO | `{code, data: ProjectConfigDTO}` | 项目管理员 | 项目不存在 |
| PROJ-API-016 | GET | `/{projectId}/delete-check` | 项目删除前检查 | projectId(UUID, Path) | `{code, data: ProjectDeleteCheckVO}` | 管理员 | 项目不存在 |

**关键DTO**:

```typescript
// ProjectQueryRequest
{ keyword?: string; status?: string; page?: number; size?: number; sortBy?: string; sortDirection?: string }

// ChangeStatusRequest
{ status: string; reason?: string }

// AddMemberRequest
{ userIds: string[]; roleInProject: string }

// ProjectDeleteCheckVO
{ canDelete: boolean; reason?: string; statistics: ProjectDeleteStatistics }

// ProjectSelectorItem
{ id: UUID; code: string; name: string; status: string }
```

---

### 3.2 SpaceNodeController -- `/api/mdm/space-nodes`

| 编号 | 方法 | 路径 | 说明 | 请求参数 | 响应格式 | 权限要求 | 例外情况 |
|------|------|------|------|----------|----------|----------|----------|
| SN-API-001 | GET | `/` | 分页查询空间节点 | page(int, 默认0), size(int, 默认10) | `{code, data: Page<SpaceNode>}` | 已登录 | 分页参数越界 |
| SN-API-002 | GET | `/{id}` | 查询节点详情 | id(UUID, Path) | `{code, data: SpaceNode}` | 已登录 | 节点不存在 |
| SN-API-003 | GET | `/project/{projectId}` | 按项目查询节点列表 | projectId(UUID, Path) | `{code, data: [SpaceNode]}` | 项目成员 | 项目不存在 |
| SN-API-004 | GET | `/project/{projectId}/tree` | 获取项目空间树 | projectId(UUID, Path) | `{code, data: [SpaceNodeTreeDTO]}` | 项目成员 | 项目不存在 |
| SN-API-005 | GET | `/project/{projectId}/roots` | 获取项目根节点 | projectId(UUID, Path) | `{code, data: [SpaceNode]}` | 项目成员 | 项目不存在 |
| SN-API-006 | GET | `/project/{projectId}/type/{nodeType}` | 按项目+类型查询 | projectId(UUID, Path), nodeType(NodeType, Path) | `{code, data: [SpaceNode]}` | 项目成员 | 无效NodeType |
| SN-API-007 | GET | `/parent/{parentId}/children` | 获取子节点列表 | parentId(UUID, Path) | `{code, data: [SpaceNode]}` | 已登录 | 父节点不存在 |
| SN-API-008 | POST | `/` | 创建空间节点 | Body: SpaceNodeCreateDTO | `{code, data: SpaceNode}` | 项目管理员 | 项目不存在; 父节点不存在; 字段校验失败 |
| SN-API-009 | POST | `/batch` | 批量创建节点 | Body: [SpaceNodeCreateDTO] | `{code, data: [SpaceNode]}` | 项目管理员 | 批量数量超限; 父节点不存在 |
| SN-API-010 | PUT | `/{id}` | 更新节点 | id(UUID, Path), Body: SpaceNodeUpdateDTO | `{code, data: SpaceNode}` | 项目管理员 | 节点不存在 |
| SN-API-011 | DELETE | `/{id}` | 删除节点 | id(UUID, Path) | `{code, data: null}` | 项目管理员 | 节点不存在; 有子节点 |
| SN-API-012 | GET | `/{id}/delete-check` | 删除前检查 | id(UUID, Path) | `{code, data: SpaceNodeDeleteCheckDTO}` | 项目管理员 | 节点不存在 |
| SN-API-013 | DELETE | `/{id}/cascade` | 级联删除(含子节点) | id(UUID, Path) | `{code, data: null}` | 项目管理员 | 节点不存在 |
| SN-API-014 | GET | `/{id}/equipment` | 获取设备详情 | id(UUID, Path) | `{code, data: SpaceNodeEquipmentDTO}` | 项目成员(@Deprecated) | 节点不存在 |
| SN-API-015 | GET | `/equipment` | 获取设备列表 | projectId(UUID, Param) | `{code, data: [SpaceNodeEquipmentDTO]}` | 项目成员(@Deprecated) | 项目不存在 |
| SN-API-016 | GET | `/special-equipment` | 获取特种设备列表 | projectId(UUID, Param) | `{code, data: [SpaceNodeEquipmentDTO]}` | 项目成员(@Deprecated) | 项目不存在 |
| SN-API-017 | GET | `/expiring-inspection` | 获取即将年检设备 | projectId(UUID, Param), daysAhead(Integer, 默认90) | `{code, data: [SpaceNodeEquipmentDTO]}` | 项目成员(@Deprecated) | 项目不存在 |
| SN-API-018 | POST | `/equipment` | 创建设备 | Body: EquipmentCreateDTO | `{code, data: SpaceNode}` | 项目管理员(@Deprecated) | 项目不存在; 字段校验失败 |
| SN-API-019 | POST | `/equipment/batch` | 批量创建设备 | Body: [EquipmentCreateDTO] | `{code, data: [SpaceNode]}` | 项目管理员(@Deprecated) | 批量数量超限 |
| SN-API-020 | POST | `/equipment/import` | Excel导入设备 | file(Multipart), projectId(UUID, Param) | `{code, data: Object}` | 项目管理员(@Deprecated) | 文件为空; 文件类型不支持; 文件超10MB; 行数超1000 |
| SN-API-021 | GET | `/{buildingId}/floor-info` | 获取楼栋楼层信息 | buildingId(String, Path) | `{code, data: FloorInfoVO}` | 项目成员 | 楼栋不存在 |
| SN-API-022 | GET | `/debug/floor-numbers` | 调试:检查房间楼层号 | projectId(UUID, Param) | `{code, data: Map}` | 管理员 | 项目不存在 |

**关键DTO**:

```typescript
// SpaceNodeCreateDTO
{ projectId: UUID; name: String; nodeCategory: NodeCategory; nodeType: NodeType; parentId?: UUID; buildingArea?: BigDecimal; ... }

// SpaceNodeTreeDTO extends SpaceNode
{ ...SpaceNode; children: SpaceNodeTreeDTO[] }

// SpaceNodeDeleteCheckDTO
{ nodeId: UUID; nodeName: String; childCount: Integer; childTypeCount: Map; totalDescendantCount: Integer }

// EquipmentCreateDTO
{ projectId: UUID; name: String; nodeType: NodeType; parentId?: UUID; ...设备扩展字段 }

// FloorInfoVO
{ buildingId: UUID; buildingName: String; totalFloors: Integer; undergroundFloors: Integer; floors: FloorDetailVO[] }
```

---

### 3.3 InspectionItemController -- `/api/mdm/inspection-items`

| 编号 | 方法 | 路径 | 说明 | 请求参数 | 响应格式 | 权限要求 | 例外情况 |
|------|------|------|------|----------|----------|----------|----------|
| II-API-001 | POST | `/` | 创建巡检标准项 | Body: InspectionItem | `{code, data: InspectionItem}` | 管理员 | 字段校验失败 |
| II-API-002 | GET | `/` | 查询巡检标准项列表 | equipmentType?(String), systemType?(String), activeOnly?(Boolean) | `{code, data: [InspectionItem]}` | 已登录 | 无 |
| II-API-003 | GET | `/{id}` | 获取标准项详情 | id(UUID, Path) | `{code, data: InspectionItem}` | 已登录 | 标准项不存在 |
| II-API-004 | PUT | `/{id}` | 更新标准项 | id(UUID, Path), Body: InspectionItem | `{code, data: InspectionItem}` | 管理员 | 标准项不存在 |
| II-API-005 | DELETE | `/{id}` | 删除标准项 | id(UUID, Path) | `{code, data: null}` | 管理员 | 标准项不存在; 已被模板引用 |

**查询逻辑优先级**:
1. activeOnly=true → 仅返回ACTIVE状态项
2. equipmentType + systemType → 双条件过滤
3. equipmentType → 单条件过滤
4. systemType → 系统类型过滤
5. 无参数 → 返回全部

---

### 3.4 InspectionTemplateController -- `/api/ops/inspection-templates`

| 编号 | 方法 | 路径 | 说明 | 请求参数 | 响应格式 | 权限要求 | 例外情况 |
|------|------|------|------|----------|----------|----------|----------|
| IT-API-001 | GET | `/` | 获取项目模板列表 | projectId(UUID, Param) | `{code, data: [InspectionTemplate]}` | 项目成员 | 项目不存在 |
| IT-API-002 | POST | `/` | 创建模板 | Body: InspectionTemplate | `{code, data: InspectionTemplate}` | 管理员 | 模板编码已存在 |
| IT-API-003 | GET | `/{id}` | 获取模板详情 | id(UUID, Path) | `{code, data: InspectionTemplate}` | 已登录 | 模板不存在 |
| IT-API-004 | PUT | `/{id}` | 更新模板 | id(UUID, Path), Body: InspectionTemplate | `{code, data: InspectionTemplate}` | 管理员 | 模板不存在 |
| IT-API-005 | POST | `/{id}/copy` | 复制模板 | id(UUID, Path), newName(String, Param) | `{code, data: InspectionTemplate}` | 管理员 | 模板不存在 |
| IT-API-006 | GET | `/by-type/{equipmentType}` | 按设备类型查模板 | equipmentType(String, Path) | `{code, data: [InspectionTemplate]}` | 已登录 | 无 |

---

### 3.5 InspectionRecordController -- `/api/mdm/inspection-records`

| 编号 | 方法 | 路径 | 说明 | 请求参数 | 响应格式 | 权限要求 | 例外情况 |
|------|------|------|------|----------|----------|----------|----------|
| IR-API-001 | POST | `/` | 创建巡检记录 | Body: InspectionRecord | `{code, data: InspectionRecord}` | 项目成员 | 设备不存在; 字段校验失败 |
| IR-API-002 | GET | `/` | 查询巡检记录列表 | equipmentId?(UUID), planId?(UUID), inspector?(String), status?(CheckStatus), startDate?(LocalDate), endDate?(LocalDate) | `{code, data: [InspectionRecord]}` | 项目成员 | 无 |
| IR-API-003 | GET | `/{id}` | 获取记录详情 | id(UUID, Path) | `{code, data: InspectionRecord}` | 已登录 | 记录不存在 |
| IR-API-004 | PUT | `/{id}` | 更新记录 | id(UUID, Path), Body: InspectionRecord | `{code, data: InspectionRecord}` | 项目成员 | 记录不存在 |
| IR-API-005 | DELETE | `/{id}` | 删除记录 | id(UUID, Path) | `{code, data: null}` | 管理员 | 记录不存在 |
| IR-API-006 | POST | `/{id}/complete` | 完成巡检记录 | id(UUID, Path) | `{code, data: null}` | 项目成员 | 记录不存在; 已完成 |

**查询逻辑优先级**:
1. equipmentId + 日期范围 → 设备+日期
2. equipmentId → 设备
3. planId → 计划
4. inspector → 巡检人
5. status → 状态
6. 日期范围 → 时间段
7. 无参数 → 全部

---

### 3.6 SparePartController -- `/api/ops/spare-parts`

| 编号 | 方法 | 路径 | 说明 | 请求参数 | 响应格式 | 权限要求 | 例外情况 |
|------|------|------|------|----------|----------|----------|----------|
| SP-API-001 | GET | `/categories` | 获取分类列表 | 无 | `{code, data: [SparePartCategory]}` | 已登录 | 无 |
| SP-API-002 | POST | `/categories` | 创建分类 | Body: SparePartCategory | `{code, data: SparePartCategory}` | 管理员 | 分类编码已存在 |
| SP-API-003 | GET | `/` | 获取备件列表 | projectId(UUID, Param), categoryId?(UUID) | `{code, data: [SparePart]}` | 项目成员 | 项目不存在 |
| SP-API-004 | GET | `/{id}` | 获取备件详情 | id(UUID, Path) | `{code, data: SparePart}` | 已登录 | 备件不存在 |
| SP-API-005 | POST | `/` | 创建备件 | Body: SparePart | `{code, data: SparePart}` | 管理员 | 备件编码已存在 |
| SP-API-006 | PUT | `/{id}` | 更新备件 | id(UUID, Path), Body: SparePart | `{code, data: SparePart}` | 管理员 | 备件不存在 |
| SP-API-007 | DELETE | `/{id}` | 删除备件 | id(UUID, Path) | `{code, data: null}` | 管理员 | 备件不存在; 有出入库记录 |
| SP-API-008 | GET | `/low-stock` | 获取低库存备件 | projectId(UUID, Param) | `{code, data: [SparePart]}` | 项目成员 | 项目不存在 |
| SP-API-009 | POST | `/in-stock` | 入库操作 | Body: StockRequest `{sparePartId, quantity, recordedBy?, remarks?}` | `{code, data: null}` | 项目成员 | 备件不存在; 数量<=0 |
| SP-API-010 | POST | `/out-stock` | 出库操作 | Body: OutStockRequest `{sparePartId, quantity, relatedOrderId?, recordedBy?, remarks?}` | `{code, data: null}` | 项目成员 | 备件不存在; 库存不足; 数量<=0 |
| SP-API-011 | GET | `/{id}/records` | 获取备件出入库记录 | id(UUID, Path) | `{code, data: [SparePartRecord]}` | 已登录 | 备件不存在 |

---

### 3.7 EnergyController -- `/api/ops/energy`

| 编号 | 方法 | 路径 | 说明 | 请求参数 | 响应格式 | 权限要求 | 例外情况 |
|------|------|------|------|----------|----------|----------|----------|
| EN-API-001 | POST | `/meters` | 创建计量点 | Body: EnergyMeter | `{code, data: EnergyMeter}` | 管理员 | 计量点编码已存在 |
| EN-API-002 | GET | `/meters` | 获取计量点列表 | projectId(UUID, Param), energyType?(String) | `{code, data: [EnergyMeter]}` | 项目成员 | 项目不存在 |
| EN-API-003 | GET | `/meters/{id}` | 获取计量点详情 | id(UUID, Path) | `{code, data: EnergyMeter}` | 已登录 | 计量点不存在 |
| EN-API-004 | PUT | `/meters/{id}` | 更新计量点 | id(UUID, Path), Body: EnergyMeter | `{code, data: EnergyMeter}` | 管理员 | 计量点不存在 |
| EN-API-005 | DELETE | `/meters/{id}` | 删除计量点 | id(UUID, Path) | `{code, data: null}` | 管理员 | 计量点不存在; 有能耗记录 |
| EN-API-006 | POST | `/consumption` | 录入能耗记录 | Body: RecordConsumptionRequest `{meterId, currentReading, recordedBy?}` | `{code, data: null}` | 项目成员 | 计量点不存在; 读数小于上次 |
| EN-API-007 | GET | `/consumption/{meterId}` | 获取能耗记录 | meterId(UUID, Path), startDate?(LocalDate), endDate?(LocalDate) | `{code, data: [EnergyConsumption]}` | 项目成员 | 计量点不存在 |
| EN-API-008 | GET | `/statistics/by-type` | 按类型统计能耗 | projectId(UUID, Param), month(String, Param) | `{code, data: Map<EnergyType, BigDecimal>}` | 项目成员 | 项目不存在 |
| EN-API-009 | GET | `/statistics/unit-consumption` | 单位面积能耗 | projectId(UUID, Param), month(String, Param) | `{code, data: BigDecimal}` | 项目成员 | 项目不存在 |

---

## 四、业务规则

### 4.1 空间节点树形管理（创建时自动生成treePath/删除前检查/级联删除）

#### 4.1.1 树形结构规则

```
项目 (Project)
├── 楼栋 (BUILDING)          ← NodeCategory.BUILDING, order=1
│   ├── 单元 (UNIT)          ← NodeCategory.BUILDING, order=2
│   │   ├── 楼层 (FLOOR)     ← NodeCategory.BUILDING, order=3
│   │   │   └── 房间 (ROOM)  ← NodeCategory.BUILDING, order=4
│   │   └── 房间 (ROOM)      ← 可跳过楼层直接挂房间
│   └── 公共用房 (PUBLIC_ROOM)
├── 商铺 (SHOP)              ← NodeCategory.BUILDING, order=2（直接挂在项目下）
├── 车库 (GARAGE)            ← NodeCategory.PARKING, order=1
│   ├── 停车区域 (PARKING_AREA) ← NodeCategory.PARKING, order=2
│   │   └── 车位 (PARKING_SPACE) ← NodeCategory.PARKING, order=3
├── 设备房 (EQUIPMENT_ROOM)  ← NodeCategory.FACILITY, order=1
├── 物业用房 (PROPERTY_OFFICE)
├── 门岗 (SECURITY_ROOM)
├── 公共区域 (PUBLIC_AREA)   ← NodeCategory.AREA, order=1
├── 绿化区域 (GREEN_AREA)
└── 道路 (ROAD)
```

#### 4.1.2 树形路径维护

- **treePath**: 物理路径，格式 `id1.id2.id3`，用于快速查询所有子孙节点
  - 创建节点时: `父节点.treePath + "." + 当前节点.id`（根节点为自身id）
  - 支持通过 `LIKE 'parentId.%'` 快速查询所有子孙

- **treePathName**: 名称路径，格式 `项目/楼栋/单元/房间`，用于展示
  - 创建节点时: `父节点.treePathName + "/" + 当前节点.name`

- **level**: 层级深度，根节点为0，每层+1
  - 创建节点时: `父节点.level + 1`

- **sortOrder**: 同级排序，默认0

#### 4.1.3 删除规则

1. **删除前检查**: 调用 `GET /{id}/delete-check` 获取子节点信息
   - 返回: `{nodeId, nodeName, childCount, childTypeCount, totalDescendantCount}`

2. **普通删除**: `DELETE /{id}`
   - 有子节点时拒绝删除
   - 软删除: 设置 `isDeleted=true`

3. **级联删除**: `DELETE /{id}/cascade`
   - 删除当前节点及所有子孙节点
   - 软删除: 所有节点设置 `isDeleted=true`

#### 4.1.4 批量操作

- **批量创建**: `POST /batch`，受 `BatchOperationValidator.validateUpdateSize()` 限制数量
- **Excel导入设备**: `POST /equipment/import`
  - 支持格式: .xlsx, .xls
  - 文件大小限制: 10MB
  - 行数限制: 1000行
  - 文件类型校验: contentType + 扩展名双重校验

---

### 4.2 项目状态流转（ACTIVE/DISABLED/PENDING/ARCHIVED的转换规则）

#### 4.2.1 状态值与转换规则

| 当前状态 | 可流转到 | 说明 |
|---------|---------|------|
| PENDING | ACTIVE | 审核通过 |
| PENDING | DISABLED | 审核拒绝 |
| ACTIVE | DISABLED | 禁用项目 |
| ACTIVE | ARCHIVED | 归档项目 |
| DISABLED | ACTIVE | 重新启用 |
| DISABLED | ARCHIVED | 归档禁用项目 |
| ARCHIVED | - | 终态，不可再变更 |

#### 4.2.2 状态变更规则

- 每次状态变更必须记录 `ProjectStatusHistory`（fromStatus, toStatus, reason, operatorId, operatorName）
- 变更接口: `PUT /{id}/status`，请求体: `{status, reason}`
- 非法状态转换抛出 BusinessException

#### 4.2.3 项目成员角色

| 角色 | 说明 |
|------|------|
| PROJECT_MANAGER | 项目经理 |
| PROJECT_ADMIN | 项目管理员 |
| OPERATION_STAFF | 运营人员 |
| FINANCE_STAFF | 财务人员 |
| VIEWER | 查看者 |

---

### 4.3 空间编码自动生成（BLD-{projSuffix}-{seq}格式）

> **当前状态**: SpaceNode实体有 `code` 字段（VARCHAR(50), @JsonIgnore），但自动编码规则尚未完整实现。

**设计规则**（参考原需求 02-SPACE_NODE_DESIGN.md）:

| NodeType | 编码格式 | 示例 |
|----------|---------|------|
| BUILDING | `BLD-{projSuffix}-{seq}` | BLD-XY-001 |
| UNIT | `BLD-{projSuffix}-{bldSeq}-{seq}` | BLD-XY-001-01 |
| FLOOR | `BLD-{projSuffix}-{bldSeq}-F{floorNo}` | BLD-XY-001-F3 |
| ROOM | `BLD-{projSuffix}-{bldSeq}-{unitSeq}-{seq}` | BLD-XY-001-01-0301 |
| GARAGE | `GRG-{projSuffix}-{seq}` | GRG-XY-001 |
| PARKING_SPACE | `GRG-{projSuffix}-{grgSeq}-{seq}` | GRG-XY-001-A001 |
| EQUIPMENT_ROOM | `EQP-{projSuffix}-{seq}` | EQP-XY-001 |

**唯一性约束**: 同项目同类型下编码唯一（原设计 `UNIQUE(project_id, code)`，当前代码未实现此约束）

---

### 4.4 巡检标准项管理（按设备类型/系统类型分类）

#### 4.4.1 标准项库

- 巡检标准项按 `equipmentType`（设备类型）和 `systemType`（系统类型）分类
- 每个标准项包含: 检查项名称、检查方法、标准值、是否必检
- 标准项状态: ACTIVE(启用) / INACTIVE(停用)
- 支持按 `activeOnly=true` 仅查询启用项

#### 4.4.2 巡检模板

- 模板按项目+设备类型组织
- 模板包含检查项列表（JSON格式存储，字段 `inspectionItems`）
- 支持模板版本管理（`version` 字段）
- 支持模板复制（`POST /{id}/copy`）
- 模板与标准项通过 `equipmentType` 逻辑关联

#### 4.4.3 巡检记录

- 记录关联设备（`equipmentId`）和计划（`planId`）
- 检查结果状态: NORMAL(正常) / WARNING(预警) / ABNORMAL(异常)
- 支持签到信息: 时间（`checkInTime`）、位置（`checkInLocation`）、照片（`checkInPhoto`）
- 检查项结果和异常问题以JSONB存储
- 完成操作: 调用 `POST /{id}/complete` 标记完成

---

### 4.5 巡检记录管理（签到/问题上报/完成确认）

#### 4.5.1 创建巡检记录流程

1. 选择设备（`equipmentId`）
2. 填写巡检日期（`inspectionDate`）和巡检人（`inspector`）
3. 逐项填写检查结果（`items` JSONB）
4. 上报异常问题（`problems` JSONB）
5. 签到: 记录时间、位置、照片

#### 4.5.2 完成确认

- 调用 `POST /{id}/complete`
- 设置 `completed=true`, `completedTime=当前时间`
- 已完成的记录不可再修改

#### 4.5.3 状态判定

- NORMAL: 所有检查项通过，无异常问题
- WARNING: 存在轻微异常
- ABNORMAL: 存在严重异常

---

## 五、执行约束

### 5.1 项目编码唯一性

- **数据库约束**: `mdm_project.code` 列 UNIQUE
- **校验规则**: 正则 `^[a-zA-Z0-9_-]+$`，长度2-50位
- **违反后果**: 创建/更新项目时抛出 DataIntegrityViolationException
- **自动生成**: `GET /generate-code` 接口生成唯一编码

### 5.2 空间节点树完整性（不能形成环/父节点必须存在）

- **约束说明**: 空间节点通过 parentId 自引用形成树形结构
- **父节点校验**: 创建/更新节点时，parentId 指向的节点必须存在且属于同一项目
- **环路检测**: 更新 parentId 时，沿 parentId 链向上遍历，检查是否会回到当前节点
- **违反后果**: 抛出 BusinessException，提示"不能将节点设置为其子节点的下级"
- **项目一致性**: 子节点的 projectId 必须与父节点一致

### 5.3 项目删除前检查（有空间节点/设备/工单时不能删除）

- **检查接口**: `GET /{projectId}/delete-check`
- **返回结果**: `ProjectDeleteCheckVO { canDelete, reason, statistics }`
- **阻止删除条件**:
  - 存在空间节点（SpaceNode）
  - 存在设备（isEquipment=true的SpaceNode）
  - 存在应收未收费用
  - 存在关联工单
- **违反后果**: canDelete=false，返回具体原因

### 5.4 空间编码唯一性（同项目同类型下唯一）

- **约束说明**: 同一项目下，相同 NodeType 的节点 code 应唯一
- **当前状态**: 数据库层面未建立 `UNIQUE(project_id, code)` 约束
- **业务层校验**: 创建/更新节点时检查同项目同类型下编码是否重复
- **违反后果**: 抛出 BusinessException

### 5.5 备件出库库存约束

- **约束说明**: 出库数量不能超过当前库存
- **校验方式**: `currentStock - quantity >= 0`
- **违反后果**: 抛出 BusinessException，提示"库存不足"

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

- **约束说明**: 本次读数必须大于上次读数
- **校验方式**: `currentReading > previousReading`
- **违反后果**: 抛出 BusinessException，提示"本次读数不能小于上次读数"

---

## 六、权限控制

### 6.1 API端点权限矩阵

| 控制器 | 端点 | 所需角色 | 数据范围过滤 |
|--------|------|----------|-------------|
| ProjectController | GET / | 已登录用户 | ALL/PROJECT(按用户项目过滤) |
| ProjectController | GET /selector | 已登录用户 | PROJECT(仅用户参与的项目) |
| ProjectController | GET /generate-code | 管理员 | 无 |
| ProjectController | GET /{id} | 项目成员 | PROJECT |
| ProjectController | GET /code/{code} | 项目成员 | PROJECT |
| ProjectController | POST / | 管理员(SYSTEM级) | 无 |
| ProjectController | PUT /{id} | 项目管理员 | PROJECT |
| ProjectController | DELETE /{id} | 管理员(SYSTEM级) | 无 |
| ProjectController | GET /{id}/members | 项目成员 | PROJECT |
| ProjectController | POST /{id}/members | 项目管理员 | PROJECT |
| ProjectController | DELETE /{id}/members/{memberId} | 项目管理员 | PROJECT |
| ProjectController | GET /{id}/statistics | 项目成员 | PROJECT |
| ProjectController | PUT /{id}/status | 管理员(SYSTEM级) | 无 |
| ProjectController | GET /{id}/config | 项目成员 | PROJECT |
| ProjectController | PUT /{id}/config | 项目管理员 | PROJECT |
| ProjectController | GET /{projectId}/delete-check | 管理员(SYSTEM级) | 无 |
| SpaceNodeController | GET / | 已登录用户 | ALL/PROJECT |
| SpaceNodeController | GET /{id} | 已登录用户 | PROJECT |
| SpaceNodeController | GET /project/{projectId}/* | 项目成员 | PROJECT |
| SpaceNodeController | POST / | 项目管理员 | PROJECT |
| SpaceNodeController | POST /batch | 项目管理员 | PROJECT |
| SpaceNodeController | PUT /{id} | 项目管理员 | PROJECT |
| SpaceNodeController | DELETE /{id} | 项目管理员 | PROJECT |
| SpaceNodeController | DELETE /{id}/cascade | 项目管理员 | PROJECT |
| SpaceNodeController | POST /equipment* | 项目管理员(@Deprecated) | PROJECT |
| SpaceNodeController | GET /{buildingId}/floor-info | 项目成员 | PROJECT |
| InspectionItemController | GET / | 已登录用户 | ALL |
| InspectionItemController | GET /{id} | 已登录用户 | ALL |
| InspectionItemController | POST / | 管理员 | 无 |
| InspectionItemController | PUT /{id} | 管理员 | 无 |
| InspectionItemController | DELETE /{id} | 管理员 | 无 |
| InspectionTemplateController | GET / | 项目成员 | PROJECT |
| InspectionTemplateController | POST / | 管理员 | 无 |
| InspectionTemplateController | GET /{id} | 已登录用户 | PROJECT |
| InspectionTemplateController | PUT /{id} | 管理员 | 无 |
| InspectionTemplateController | POST /{id}/copy | 管理员 | 无 |
| InspectionRecordController | POST / | 项目成员 | PROJECT |
| InspectionRecordController | GET / | 项目成员 | PROJECT |
| InspectionRecordController | GET /{id} | 已登录用户 | PROJECT |
| InspectionRecordController | PUT /{id} | 项目成员 | PROJECT |
| InspectionRecordController | DELETE /{id} | 管理员 | 无 |
| InspectionRecordController | POST /{id}/complete | 项目成员 | PROJECT |
| SparePartController | GET /categories | 已登录用户 | ALL |
| SparePartController | POST /categories | 管理员 | 无 |
| SparePartController | GET / | 项目成员 | PROJECT |
| SparePartController | GET /{id} | 已登录用户 | ALL |
| SparePartController | POST / | 管理员 | PROJECT |
| SparePartController | PUT /{id} | 管理员 | PROJECT |
| SparePartController | DELETE /{id} | 管理员 | PROJECT |
| SparePartController | GET /low-stock | 项目成员 | PROJECT |
| SparePartController | POST /in-stock | 项目成员 | PROJECT |
| SparePartController | POST /out-stock | 项目成员 | PROJECT |
| SparePartController | GET /{id}/records | 已登录用户 | ALL |
| EnergyController | POST /meters | 管理员 | PROJECT |
| EnergyController | GET /meters | 项目成员 | PROJECT |
| EnergyController | GET /meters/{id} | 已登录用户 | ALL |
| EnergyController | PUT /meters/{id} | 管理员 | PROJECT |
| EnergyController | DELETE /meters/{id} | 管理员 | PROJECT |
| EnergyController | POST /consumption | 项目成员 | PROJECT |
| EnergyController | GET /consumption/{meterId} | 项目成员 | PROJECT |
| EnergyController | GET /statistics/* | 项目成员 | PROJECT |

---

## 七、例外情况处理

### 7.1 项目相关例外

| 例外场景 | 错误码 | 错误信息 | 处理方式 |
|---------|--------|---------|---------|
| 项目不存在 | 404 | "项目不存在" | findById返回空时抛出 |
| 项目编码已存在 | 409 | "项目编码{code}已存在" | 创建/更新前查询 |
| 项目编码格式错误 | 400 | "项目代码只能包含字母、数字、连字符和下划线" | 正则校验 |
| 项目名称为空 | 400 | "项目名称不能为空" | @NotNull校验 |
| 联系电话格式错误 | 400 | "联系电话格式不正确" | 正则校验 |
| 非法状态转换 | 400 | "不允许从{from}变更为{to}" | 状态机校验 |
| 删除项目有空间节点 | 400 | "项目下存在空间节点，无法删除" | delete-check检查 |
| 删除项目有应收费用 | 400 | "项目存在应收未收费用，无法删除" | delete-check检查 |
| 项目已归档不可变更 | 400 | "已归档项目不可操作" | ARCHIVED终态检查 |

### 7.2 空间节点相关例外

| 例外场景 | 错误码 | 错误信息 | 处理方式 |
|---------|--------|---------|---------|
| 节点不存在 | 404 | "空间节点不存在" | findById返回空时抛出 |
| 父节点不存在 | 400 | "父节点不存在" | 创建/更新时校验parentId |
| 父节点不属于同一项目 | 400 | "父节点不属于当前项目" | projectId一致性校验 |
| 有子节点不能普通删除 | 400 | "该节点存在{count}个子节点，请使用级联删除" | delete-check检查 |
| 树形结构形成环 | 400 | "不能将节点设置为其子节点的下级" | parentId链遍历检查 |
| 节点名称为空 | 400 | "空间节点名称不能为空" | @NotNull校验 |
| nodeType与nodeCategory不匹配 | 400 | "节点类型与节点大类不匹配" | 枚举关联校验 |
| 批量创建数量超限 | 400 | "批量操作数量超过限制" | BatchOperationValidator校验 |

### 7.3 设备相关例外（@Deprecated）

| 例外场景 | 错误码 | 错误信息 | 处理方式 |
|---------|--------|---------|---------|
| 设备不存在 | 404 | "设备不存在" | findById返回空时抛出 |
| Excel文件为空 | 400 | "文件不能为空" | file.isEmpty()检查 |
| Excel文件类型不支持 | 400 | "不支持的文件类型，仅支持 Excel 文件（.xlsx, .xls）" | contentType+扩展名校验 |
| Excel文件过大 | 400 | "文件大小不能超过 10MB" | file.getSize()检查 |
| Excel行数超限 | 400 | "Excel 行数不能超过 1000 行，当前 {n} 行" | POI解析行数检查 |
| Excel解析失败 | 400 | "文件解析失败: {message}" | IOException捕获 |

### 7.4 巡检相关例外

| 例外场景 | 错误码 | 错误信息 | 处理方式 |
|---------|--------|---------|---------|
| 巡检标准项不存在 | 404 | "巡检标准项不存在" | findById返回空时抛出 |
| 标准项已被模板引用 | 400 | "该标准项已被巡检模板引用，无法删除" | 检查模板引用 |
| 巡检记录不存在 | 404 | "巡检记录不存在" | findById返回空时抛出 |
| 巡检记录已完成 | 400 | "巡检记录已完成，不可修改" | completed=true检查 |
| 设备ID不存在 | 400 | "设备不存在" | equipmentId校验 |
| 巡检模板编码已存在 | 409 | "模板编码{code}已存在" | 创建前查询 |
| 巡检模板不存在 | 404 | "巡检模板不存在" | findById返回空时抛出 |

### 7.5 备件相关例外

| 例外场景 | 错误码 | 错误信息 | 处理方式 |
|---------|--------|---------|---------|
| 备件不存在 | 404 | "备件不存在" | findById返回空时抛出 |
| 备件编码已存在 | 409 | "备件编码{code}已存在" | 创建前查询 |
| 出库数量超过库存 | 400 | "库存不足，当前库存{stock}，出库数量{qty}" | currentStock校验 |
| 出入库数量为0或负数 | 400 | "数量必须大于0" | quantity > 0校验 |
| 分类编码已存在 | 409 | "分类编码{code}已存在" | 创建前查询 |
| 备件有出入库记录不能删除 | 400 | "该备件存在出入库记录，无法删除" | 检查SparePartRecord |

### 7.6 能耗相关例外

| 例外场景 | 错误码 | 错误信息 | 处理方式 |
|---------|--------|---------|---------|
| 计量点不存在 | 404 | "计量点不存在" | findById返回空时抛出 |
| 计量点编码已存在 | 409 | "计量点编码{code}已存在" | 创建前查询 |
| 本次读数小于上次读数 | 400 | "本次读数不能小于上次读数" | currentReading > previousReading |
| 计量点有能耗记录不能删除 | 400 | "该计量点存在能耗记录，无法删除" | 检查EnergyConsumption |
| 项目无建筑面积 | 400 | "项目无建筑面积数据，无法计算单位面积能耗" | 统计时校验 |

### 7.7 通用例外

| 例外场景 | 错误码 | 错误信息 | 处理方式 |
|---------|--------|---------|---------|
| 分页参数越界 | 200(自动修正) | 无(使用getSafeSize修正) | PaginationValidator校验 |
| 项目ID为空 | 400 | "项目ID不能为空" | @NotNull校验 |
| 无权限访问项目数据 | 403 | "无权访问该项目数据" | DataScopeService校验 |
