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

1009 lines
50 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.

# 空间与项目域 - 详细设计
**文档版本**: 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校验 |