ether-docs/_archive/domains-old/02-SPACE_NODE_DESIGN.md

826 lines
38 KiB
Markdown
Raw 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.

# Ether 空间节点管理架构设计方案
**版本**: v2.1
**设计日期**: 2026-02-16
**设计目标**: 重构空间节点管理,统一空间体系,支持地图服务对接
**最后更新**: 2026-04-26
> **更新记录**:
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更树形查询改为按项目维度类型查询统一为按类型查询新增删除前检查和级联删除新增楼层信息查询新增软删除isDeleted字段NodeType增加PUBLIC_ROOM标注TODO: SpaceNode需增加code字段和唯一约束标注TODO: 设备扩展字段应统一到Equipment独立表API路径去掉v1版本号。
---
## 一、业务背景与需求分析
### 1.1 物业行业空间管理标准
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 物业项目空间层级标准模型 │
└─────────────────────────────────────────────────────────────────────────────┘
项目 (Project)
├── 住宅区域 (Residential Area)
│ ├── 分期 (Phase) ──────────────────── 可选层级,大型项目使用
│ │ ├── 楼栋 (Building)
│ │ │ ├── 单元 (Unit)
│ │ │ │ ├── 楼层 (Floor) ──────── 可选层级
│ │ │ │ │ └── 房间 (Room)
│ │ │ │ └── 房间 (Room)
│ │ │ └── 公共区域 (Public Area)
│ │ └── 地下车库 (Underground Garage)
│ │ └── 车位 (Parking Space)
│ └── 地面停车场 (Surface Parking)
│ └── 车位 (Parking Space)
├── 商业区域 (Commercial Area)
│ ├── 商铺 (Shop)
│ └── 公共区域 (Public Area)
├── 公共设施区域 (Facility Area)
│ ├── 设备房 (Equipment Room)
│ ├── 物业用房 (Property Office)
│ └── 配套设施 (Supporting Facility)
└── 室外区域 (Outdoor Area)
├── 道路 (Road)
├── 绿化带 (Green Belt)
└── 景观设施 (Landscape)
```
### 1.2 核心业务需求
| 需求分类 | 具体需求 | 优先级 |
|----------|----------|--------|
| **空间层级管理** | 支持项目→楼栋→单元→房间的层级结构 | P0 |
| **空间类型管理** | 支持楼栋、单元、房间、车位、商铺、公共区域等类型 | P0 |
| **项目统计** | 自动统计楼栋数、户数、车位数等 | P0 |
| **批量导入** | Excel批量导入楼栋、单元、房间数据 | P0 |
| **地图定位** | 支持高德地图标注点位和区域 | P1 |
| **空间编码** | 自动生成空间编码规则 | P1 |
| **业务关联** | 与工单、巡检、收费等业务模块关联 | P1 |
| **可视化展示** | 树形结构展示、地图展示 | P1 |
### 1.3 地图服务需求
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 地图服务对接需求 │
└─────────────────────────────────────────────────────────────────────────────┘
1. 点位标注 (Point)
- 楼栋位置:标注楼栋在地图上的位置
- 车位位置:标注车位在地图上的位置
- 设施位置:标注公共设施的位置
- 巡检点:标注巡检点位
2. 区域绘制 (Polygon)
- 项目边界:绘制项目的地理边界
- 区域划分:绘制管理区域边界
- 车位区域:绘制停车场区域
3. 路径规划 (Polyline)
- 巡检路线:规划巡检路线
- 巡逻路线:规划安保巡逻路线
4. 地图交互
- 点击查询:点击地图元素查看详情
- 区域筛选:在地图上框选筛选
- 距离测量:测量两点间距离
```
---
## 二、数据模型设计
### 2.1 统一空间节点模型
**设计原则**
1. 所有空间实体统一使用 `mdm_space_node`
2. 通过 `node_type` 区分不同类型
3. 通过 `node_category` 区分大类(建筑、车位、设施)
4. 支持树形结构和地图数据
```sql
-- ============================================================
-- 空间节点主表 (统一管理所有空间实体)
-- ============================================================
CREATE TABLE mdm_space_node (
-- 主键
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- 基础属性
project_id UUID NOT NULL REFERENCES mdm_project(id),
code VARCHAR(50) NOT NULL, -- 空间编码
name VARCHAR(100) NOT NULL, -- 空间名称
full_name VARCHAR(500), -- 全路径名称
short_name VARCHAR(50), -- 简称
-- 类型分类
node_category VARCHAR(20) NOT NULL, -- 节点大类: BUILDING/PARKING/FACILITY/AREA
node_type VARCHAR(30) NOT NULL, -- 节点类型: BUILDING/UNIT/ROOM/PARKING/SHOP等
usage_type VARCHAR(30), -- 用途类型: RESIDENTIAL/COMMERCIAL/OFFICE等
-- 树形结构
parent_id UUID REFERENCES mdm_space_node(id),
tree_path VARCHAR(1000), -- 物理路径: id.id.id
tree_path_name VARCHAR(1000), -- 名称路径: 项目/楼栋/单元/房间
level INTEGER DEFAULT 0, -- 层级深度
sort_order INTEGER DEFAULT 0, -- 排序号
-- 状态管理
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
delivery_status VARCHAR(20), -- 交付状态: UNDELIVERED/DELIVERED
decoration_status VARCHAR(20), -- 装修状态: ROUGH/FINE/UNDONE
-- 面积信息
building_area NUMERIC(10,2), -- 建筑面积(㎡)
usable_area NUMERIC(10,2), -- 使用面积(㎡)
shared_area NUMERIC(10,2), -- 公摊面积(㎡)
land_area NUMERIC(10,2), -- 占地面积(㎡)
-- 地理信息
longitude NUMERIC(10,6), -- 经度
latitude NUMERIC(10,6), -- 纬度
location GEOMETRY(Point, 4326), -- PostGIS点位
boundary GEOMETRY(Polygon, 4326), -- PostGIS区域边界
altitude NUMERIC(8,2), -- 海拔高度
floor_number INTEGER, -- 楼层号(正数地上,负数地下)
-- 地址信息
province VARCHAR(50), -- 省
city VARCHAR(50), -- 市
district VARCHAR(50), -- 区
street VARCHAR(100), -- 街道
address VARCHAR(255), -- 详细地址
-- 扩展属性(JSON)
attributes JSONB, -- 类型特定属性
-- 系统字段
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by UUID,
updated_by UUID,
is_deleted BOOLEAN DEFAULT FALSE, -- 软删除标记(实际代码使用 isDeleted 字段)
-- 约束
CONSTRAINT uk_space_node_project_code UNIQUE (project_id, code)
);
-- 索引
CREATE INDEX idx_space_node_project ON mdm_space_node(project_id);
CREATE INDEX idx_space_node_parent ON mdm_space_node(parent_id);
CREATE INDEX idx_space_node_type ON mdm_space_node(node_type);
CREATE INDEX idx_space_node_tree_path ON mdm_space_node(tree_path);
CREATE INDEX idx_space_node_location ON mdm_space_node USING GIST(location);
CREATE INDEX idx_space_node_boundary ON mdm_space_node USING GIST(boundary);
CREATE INDEX idx_space_node_attributes ON mdm_space_node USING GIN(attributes);
```
### 2.2 节点类型与属性定义
```java
/**
* 空间节点大类
*/
public enum SpaceNodeCategory {
BUILDING("建筑空间", "楼栋、单元、房间等建筑空间"),
PARKING("停车空间", "车位、车库等停车空间"),
FACILITY("设施空间", "设备房、公共设施等"),
AREA("区域空间", "公共区域、管理区域等");
}
/**
* 空间节点类型
*/
public enum SpaceNodeType {
// 建筑空间
BUILDING("楼栋", SpaceNodeCategory.BUILDING, 1),
UNIT("单元", SpaceNodeCategory.BUILDING, 2),
FLOOR("楼层", SpaceNodeCategory.BUILDING, 3),
ROOM("房间", SpaceNodeCategory.BUILDING, 4),
PUBLIC_ROOM("公共房间", SpaceNodeCategory.BUILDING, 4),
SHOP("商铺", SpaceNodeCategory.BUILDING, 2),
// 停车空间
GARAGE("车库", SpaceNodeCategory.PARKING, 1),
PARKING_AREA("停车区域", SpaceNodeCategory.PARKING, 2),
PARKING_SPACE("车位", SpaceNodeCategory.PARKING, 3),
// 设施空间
EQUIPMENT_ROOM("设备房", SpaceNodeCategory.FACILITY, 1),
PROPERTY_OFFICE("物业用房", SpaceNodeCategory.FACILITY, 1),
SECURITY_ROOM("门岗", SpaceNodeCategory.FACILITY, 1),
// 区域空间
PUBLIC_AREA("公共区域", SpaceNodeCategory.AREA, 1),
GREEN_AREA("绿化区域", SpaceNodeCategory.AREA, 1),
ROAD("道路", SpaceNodeCategory.AREA, 1);
}
```
### 2.3 类型特定属性 (JSONB)
```json
// 楼栋属性
{
"buildingType": "RESIDENTIAL", // 楼栋类型: RESIDENTIAL/COMMERCIAL/MIXED
"structureType": "FRAME", // 结构类型: FRAME/BRICK/STEEL
"totalFloors": 18, // 总层数
"undergroundFloors": 1, // 地下层数
"totalUnits": 4, // 单元数
"totalRooms": 144, // 房间数
"elevatorCount": 2, // 电梯数
"completionDate": "2020-06-30", // 竣工日期
"developer": "XX房地产开发公司" // 开发商
}
// 单元属性
{
"unitType": "STANDARD", // 单元类型
"elevatorCount": 1, // 电梯数
"stairType": "DOUBLE_RUN", // 楼梯类型
"roomsPerFloor": 2 // 每层户数
}
// 房间属性
{
"roomType": "APARTMENT", // 房间类型: APARTMENT/VILLA/SHOP/OFFICE
"layoutType": "3T2", // 户型: 3室2厅2卫
"roomCount": 3, // 卧室数
"livingRoomCount": 2, // 客厅数
"bathroomCount": 2, // 卫生间数
"kitchenCount": 1, // 厨房数
"balconyCount": 1, // 阳台数
"orientation": "SOUTH", // 朝向
"propertyCertificateNo": "沪房地徐字(2020)第0001号" // 房产证号
}
// 车位属性
{
"parkingType": "UNDERGROUND", // 车位类型: UNDERGROUND/SURFACE/MECHANICAL
"parkingCategory": "STANDARD", // 车位类别: STANDARD/LARGE/DISABLED
"vehicleType": "CAR", // 车辆类型: CAR/SUV/VAN
"hasChargingPile": false, // 是否有充电桩
"zoneCode": "A", // 区域编码
"rowNumber": 1, // 排号
"columnNumber": 5 // 列号
}
// 商铺属性
{
"shopType": "RETAIL", // 商铺类型: RETAIL/CATERING/SERVICE
"frontage": 8.5, // 门面宽度(米)
"depth": 12.0, // 进深(米)
"ceilingHeight": 4.5, // 层高(米)
"hasMezzanine": false, // 是否有夹层
"businessLicense": "XXX" // 营业执照
}
```
### 2.4 项目统计视图
```sql
-- 项目统计视图(实时计算)
CREATE OR REPLACE VIEW v_project_statistics AS
SELECT
p.id AS project_id,
p.code AS project_code,
p.name AS project_name,
-- 建筑统计
COUNT(DISTINCT CASE WHEN sn.node_type = 'BUILDING' THEN sn.id END) AS total_buildings,
COUNT(DISTINCT CASE WHEN sn.node_type = 'UNIT' THEN sn.id END) AS total_units,
COUNT(DISTINCT CASE WHEN sn.node_type = 'ROOM' THEN sn.id END) AS total_rooms,
-- 车位统计
COUNT(DISTINCT CASE WHEN sn.node_type = 'PARKING_SPACE' THEN sn.id END) AS total_parking,
COUNT(DISTINCT CASE WHEN sn.node_type = 'PARKING_SPACE' AND ps.status = 'AVAILABLE' THEN sn.id END) AS available_parking,
-- 面积统计
SUM(CASE WHEN sn.node_type = 'ROOM' THEN sn.building_area ELSE 0 END) AS total_building_area,
SUM(CASE WHEN sn.node_type = 'ROOM' THEN sn.usable_area ELSE 0 END) AS total_usable_area,
-- 入住统计
COUNT(DISTINCT CASE WHEN sn.node_type = 'ROOM' AND sn.status = 'OCCUPIED' THEN sn.id END) AS occupied_rooms,
COUNT(DISTINCT CASE WHEN sn.node_type = 'ROOM' AND sn.status = 'VACANT' THEN sn.id END) AS vacant_rooms,
p.updated_at
FROM mdm_project p
LEFT JOIN mdm_space_node sn ON sn.project_id = p.id AND sn.is_deleted = FALSE
GROUP BY p.id, p.code, p.name, p.updated_at;
```
---
## 三、API接口设计
### 3.1 RESTful API 规范
```
基础路径: /api/mdm/space-nodes
┌─────────────────────────────────────────────────────────────────────────────┐
│ 空间节点 API 接口 │
└─────────────────────────────────────────────────────────────────────────────┘
1. 基础 CRUD
POST /api/mdm/space-nodes 创建空间节点
PUT /api/mdm/space-nodes/{id} 更新空间节点
DELETE /api/mdm/space-nodes/{id} 删除空间节点(含删除前检查)
GET /api/mdm/space-nodes/{id} 查询空间节点详情
2. 树形结构(按项目维度查询)
GET /api/mdm/space-nodes/tree?projectId={id} 获取项目空间树
GET /api/mdm/space-nodes/roots?projectId={id} 获取项目根节点列表
GET /api/mdm/space-nodes/{id}/children 获取子节点列表
GET /api/mdm/space-nodes/{id}/ancestors 获取祖先节点链
GET /api/mdm/space-nodes/{id}/descendants 获取所有子孙节点
PUT /api/mdm/space-nodes/{id}/move 移动节点到新父节点
3. 类型查询(统一为按类型查询)
GET /api/mdm/space-nodes?type={nodeType} 按类型查询空间节点
4. 楼层信息查询(新增)
GET /api/mdm/space-nodes/{id}/floors 获取楼栋下的楼层列表
5. 批量操作
POST /api/mdm/space-nodes/batch 批量创建
PUT /api/mdm/space-nodes/batch 批量更新
DELETE /api/mdm/space-nodes/batch 批量删除
POST /api/mdm/space-nodes/import Excel导入
GET /api/mdm/space-nodes/export/template 下载导入模板
GET /api/mdm/space-nodes/export 导出数据
6. 地图相关
GET /api/mdm/space-nodes/map/markers 获取地图标注点
GET /api/mdm/space-nodes/map/boundaries 获取区域边界
PUT /api/mdm/space-nodes/{id}/location 更新位置信息
PUT /api/mdm/space-nodes/{id}/boundary 更新区域边界
7. 统计分析
GET /api/mdm/space-nodes/statistics 空间统计数据
GET /api/mdm/space-nodes/statistics/by-type 按类型统计
GET /api/mdm/space-nodes/statistics/by-status 按状态统计
```
> **变更说明**:
> - API路径已去掉v1版本号统一为 `/api/mdm/space-nodes`
> - 树形查询改为按项目维度(`tree?projectId={id}`),不再返回全量树
> - 类型查询统一为 `?type={nodeType}` 参数化查询,替代原有的 `/buildings`、`/rooms` 等独立端点
> - 新增删除前检查:删除节点前检查是否有子节点和关联业务数据
> - 新增级联删除:支持级联删除子节点
> - 新增楼层信息查询:`GET /{id}/floors`
### 3.2 请求/响应示例
**创建楼栋**:
```json
// POST /api/mdm/space-nodes
{
"projectId": "uuid",
"code": "B001",
"name": "1号楼",
"nodeCategory": "BUILDING",
"nodeType": "BUILDING",
"parentId": null,
"buildingArea": 12000.00,
"longitude": 121.473701,
"latitude": 31.230416,
"attributes": {
"buildingType": "RESIDENTIAL",
"totalFloors": 18,
"undergroundFloors": 1,
"totalUnits": 4,
"elevatorCount": 2
}
}
```
**创建单元**:
```json
{
"projectId": "uuid",
"code": "B001-U1",
"name": "1单元",
"nodeCategory": "BUILDING",
"nodeType": "UNIT",
"parentId": "building-uuid",
"attributes": {
"unitType": "STANDARD",
"elevatorCount": 1,
"roomsPerFloor": 2
}
}
```
**批量创建房间**:
```json
// POST /api/mdm/space-nodes/batch
{
"parentId": "unit-uuid",
"nodeType": "ROOM",
"startFloor": 1,
"endFloor": 18,
"roomsPerFloor": 2,
"roomPrefix": "B001-1-",
"roomTemplate": {
"nodeCategory": "BUILDING",
"buildingArea": 89.5,
"usableArea": 72.3,
"attributes": {
"roomType": "APARTMENT",
"layoutType": "3T2",
"roomCount": 3,
"livingRoomCount": 2,
"bathroomCount": 2
}
}
}
```
---
## 四、前端界面设计
### 4.1 页面结构
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 空间节点管理页面 │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 工具栏: [新增楼栋] [批量导入] [导出] [切换视图] [地图模式] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ ┌───────────────────────┬─────────────────────────────────────────────────┐ │
│ │ │ │ │
│ │ 树形导航 (左侧) │ 内容区域 (右侧) │ │
│ │ │ │ │
│ │ 📁 测试小区一期 │ ┌─────────────────────────────────────────┐ │ │
│ │ ├── 🏢 1号楼 │ │ 楼栋详情: 1号楼 │ │ │
│ │ │ ├── 🚪 1单元 │ │ ─────────────────────────────────────── │ │ │
│ │ │ │ ├── 🏠 101 │ │ 编码: B001 │ │ │
│ │ │ │ ├── 🏠 102 │ │ 楼层: 18层 (地下1层) │ │ │
│ │ │ │ └── ... │ │ 单元: 4个 │ │ │
│ │ │ ├── 🚪 2单元 │ │ 户数: 144户 │ │ │
│ │ │ └── ... │ │ 电梯: 2部 │ │ │
│ │ ├── 🏢 2号楼 │ │ │ │ │
│ │ ├── 🅿️ 地下车库 │ │ [编辑] [添加单元] [批量添加房间] [删除] │ │ │
│ │ │ ├── 🅿️ A区 │ └─────────────────────────────────────────┘ │ │
│ │ │ └── 🅿️ B区 │ │ │
│ │ └── 🏪 商业街 │ ┌─────────────────────────────────────────┐ │ │
│ │ ├── 🏪 S001 │ │ 子节点列表 │ │ │
│ │ └── 🏪 S002 │ │ ─────────────────────────────────────── │ │ │
│ │ │ │ [表格: 单元列表/房间列表] │ │ │
│ │ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────┴─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 4.2 地图模式
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 地图模式 │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 工具栏: [绘制项目边界] [标注楼栋] [标注车位] [标注设施] [保存] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────┐ │ │
│ │ │ 1# │ │ │
│ │ └─────┘ ┌─────┐ │ │
│ │ │ 2# │ │ │
│ │ ┌─────┐ └─────┘ │ │
│ │ │ 3# │ │ │
│ │ └─────┘ ┌──────────────────┐ │ │
│ │ │ 地下车库入口 │ │ │
│ │ ┌─────┐ └──────────────────┘ │ │
│ │ │ 4# │ │ │
│ │ └─────┘ │ │
│ │ │ │
│ │ ═════════════════════════════════════════════════════════════════ │ │
│ │ 小区主干道 │ │
│ │ ═════════════════════════════════════════════════════════════════ │ │
│ │ │ │
│ │ 🌳🌳🌳 🌳🌳🌳 │ │
│ │ 绿化带 绿化带 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 图层控制: [☑ 楼栋] [☑ 车位] [☑ 设施] [☑ 区域边界] [☐ 巡检路线] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 4.3 批量导入模板
**楼栋导入模板**:
| 楼栋编码* | 楼栋名称* | 总层数 | 地下层数 | 单元数 | 建筑面积 | 经度 | 纬度 |
|-----------|-----------|--------|----------|--------|----------|------|------|
| B001 | 1号楼 | 18 | 1 | 4 | 12000 | 121.473701 | 31.230416 |
| B002 | 2号楼 | 18 | 1 | 4 | 12000 | 121.473801 | 31.230516 |
**房间导入模板**:
| 楼栋编码* | 单元编码 | 楼层* | 房间编码* | 房间名称* | 建筑面积 | 使用面积 | 户型 | 房间数 | 客厅数 | 卫生间数 |
|-----------|----------|-------|-----------|-----------|----------|----------|------|--------|--------|----------|
| B001 | U1 | 1 | B001-1-101 | 101室 | 89.5 | 72.3 | 3T2 | 3 | 2 | 2 |
| B001 | U1 | 1 | B001-1-102 | 102室 | 89.5 | 72.3 | 3T2 | 3 | 2 | 2 |
---
## 五、地图服务对接方案
### 5.1 高德地图集成架构
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 地图服务集成架构 │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 前端应用 │────▶│ 高德地图 JS API │────▶│ 高德地图服务 │
│ (Vue3) │ │ (AMap SDK) │ │ (云端) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 后端服务 │ │ 地图数据存储 │
│ (Spring Boot) │ │ (PostGIS) │
└──────────────────┘ └──────────────────┘
│ │
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ 数据同步流程 │
│ │
│ 1. 前端使用高德地图SDK进行标注/绘制 │
│ 2. 获取标注点经纬度或区域坐标数组 │
│ 3. 提交到后端API保存 │
│ 4. 后端存储到PostGIS字段(location/boundary) │
│ 5. 支持空间查询(范围内搜索、距离计算等) │
└──────────────────────────────────────────────────────────────────────────┘
```
### 5.2 地图服务接口设计
```java
/**
* 地图服务接口
*/
public interface MapService {
/**
* 地理编码:地址转坐标
*/
GeoLocation geocode(String address);
/**
* 逆地理编码:坐标转地址
*/
String reverseGeocode(BigDecimal longitude, BigDecimal latitude);
/**
* 距离计算
*/
BigDecimal calculateDistance(BigDecimal lng1, BigDecimal lat1,
BigDecimal lng2, BigDecimal lat2);
/**
* 范围查询:查找指定范围内的空间节点
*/
List<SpaceNode> findWithinRadius(BigDecimal longitude, BigDecimal latitude,
double radiusMeters);
/**
* 多边形查询:查找多边形区域内的空间节点
*/
List<SpaceNode> findWithinPolygon(List<GeoPoint> polygon);
}
/**
* 高德地图实现
*/
@Service
public class AmapMapServiceImpl implements MapService {
@Value("${amap.api.key}")
private String apiKey;
@Value("${amap.api.geocode-url}")
private String geocodeUrl;
// ... 实现方法
}
```
### 5.3 前端地图组件
```vue
<!-- MapEditor.vue -->
<template>
<div class="map-editor">
<!-- 地图容器 -->
<div ref="mapContainer" class="map-container"></div>
<!-- 工具栏 -->
<div class="map-toolbar">
<a-space>
<a-button @click="setMode('marker')" :type="mode === 'marker' ? 'primary' : 'default'">
标注点位
</a-button>
<a-button @click="setMode('polygon')" :type="mode === 'polygon' ? 'primary' : 'default'">
绘制区域
</a-button>
<a-button @click="clearSelection">清除</a-button>
<a-button type="primary" @click="saveLocation">保存</a-button>
</a-space>
</div>
</div>
</template>
<script setup lang="ts">
import AMapLoader from '@amap/amap-jsapi-loader';
const props = defineProps<{
modelValue?: {
longitude?: number;
latitude?: number;
boundary?: Array<{ longitude: number; latitude: number }>;
};
}>();
const emit = defineEmits<{
'update:modelValue': [value: any];
}>();
let map: any = null;
let marker: any = null;
let polygon: any = null;
let AMap: any = null;
onMounted(async () => {
AMap = await AMapLoader.load({
key: 'YOUR_AMAP_KEY',
version: '2.0',
plugins: ['AMap.Marker', 'AMap.Polygon', 'AMap.Geocoder']
});
map = new AMap.Map(mapContainer.value, {
zoom: 15,
center: [121.473701, 31.230416]
});
// 初始化已有标注
if (props.modelValue?.longitude && props.modelValue?.latitude) {
addMarker(props.modelValue.longitude, props.modelValue.latitude);
}
});
function addMarker(lng: number, lat: number) {
if (marker) marker.setMap(null);
marker = new AMap.Marker({
position: [lng, lat],
draggable: true
});
marker.setMap(map);
map.setCenter([lng, lat]);
}
function saveLocation() {
const result: any = {};
if (marker) {
const pos = marker.getPosition();
result.longitude = pos.lng;
result.latitude = pos.lat;
}
if (polygon) {
const path = polygon.getPath();
result.boundary = path.map((p: any) => ({
longitude: p.lng,
latitude: p.lat
}));
}
emit('update:modelValue', result);
}
</script>
```
---
## 六、实施计划
### 6.1 阶段划分
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 实施阶段规划 │
└─────────────────────────────────────────────────────────────────────────────┘
阶段一:基础重构 (P0, 预计2周)
├── 数据模型重构
│ ├── 扩展 mdm_space_node 表结构
│ ├── 迁移 ParkingSpace 到 SpaceNode
│ └── 创建统计视图
├── API 接口重构
│ ├── 统一字段命名
│ ├── 完善树形操作接口
│ └── 添加批量操作接口
└── 前端页面重构
├── 树形组件集成
├── 表单字段对齐
└── 列表页面优化
阶段二:批量导入 (P0, 预计1周)
├── 导入模板设计
├── Excel解析服务
├── 数据校验逻辑
└── 导入结果反馈
阶段三:地图服务 (P1, 预计2周)
├── 高德地图SDK集成
├── 地图编辑组件
├── 位置数据存储
└── 空间查询功能
阶段四:业务关联 (P1, 预计1周)
├── 工单关联空间节点
├── 巡检点关联空间节点
├── 收费项目关联空间节点
└── 能耗计量点关联
```
### 6.2 技术依赖
| 依赖 | 版本 | 用途 |
|------|------|------|
| PostGIS | 3.x | 空间数据存储和查询 |
| 高德地图 JS API | 2.0 | 前端地图展示和编辑 |
| Apache POI | 5.x | Excel导入导出 |
| EasyExcel | 3.x | 大数据量Excel处理 |
---
## 七、风险与对策
| 风险 | 影响 | 对策 |
|------|------|------|
| 数据迁移风险 | 现有数据丢失 | 先备份,编写迁移脚本,测试验证 |
| 性能风险 | 大型项目查询慢 | 使用CTE递归优化添加缓存 |
| 地图服务费用 | 高德API调用收费 | 控制调用频率,使用本地缓存 |
---
## 八、开发任务清单 (2026-03-23)
### 8.1 后端任务
| 任务 | 状态 | 说明 |
|------|------|------|
| 重构 SpaceNode 实体 | 待开发 | 按设计文档完善字段 |
| 重构 SpaceNodeType 枚举 | 待开发 | 添加 BUILDING/UNIT/ROOM 等 |
| 重构 SpaceNodeService | 待开发 | 树形结构操作 |
| 重构 SpaceNodeRepository | 待开发 | JPA 查询方法 |
| 重构 SpaceNodeController | 待开发 | RESTful API |
| 添加 DTO 类 | 待开发 | 创建/更新/响应 DTO |
| 编写单元测试 | 待开发 | TDD 模式 |
### 8.2 前端任务
| 任务 | 状态 | 说明 |
|------|------|------|
| 创建 space.ts 类型定义 | 待开发 | TypeScript 类型 |
| 创建 space.ts API | 待开发 | API 函数 |
| 创建空间管理页面 | 待开发 | Space.vue |
| 创建空间树形组件 | 待开发 | 左侧树导航 |
| 创建空间表单组件 | 待开发 | 新增/编辑 |
| E2E 测试验证 | 待开发 | Playwright 测试 |
### 8.3 API 接口清单
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取空间树 | GET | /api/mdm/space-nodes/tree | 项目空间树(按项目维度) |
| 获取根节点 | GET | /api/mdm/space-nodes/roots | 项目下根节点 |
| 获取节点详情 | GET | /api/mdm/space-nodes/{id} | 节点详情 |
| 创建节点 | POST | /api/mdm/space-nodes | 创建空间节点 |
| 更新节点 | PUT | /api/mdm/space-nodes/{id} | 更新节点 |
| 删除节点 | DELETE | /api/mdm/space-nodes/{id} | 删除节点(含删除前检查和级联删除) |
| 获取子节点 | GET | /api/mdm/space-nodes/{id}/children | 子节点列表 |
| 获取楼层列表 | GET | /api/mdm/space-nodes/{id}/floors | 楼栋下楼层列表(新增) |
| 按类型查询 | GET | /api/mdm/space-nodes?type={type} | 按类型查询(统一接口) |
| 批量创建 | POST | /api/mdm/space-nodes/batch | 批量创建 |
| 批量导入 | POST | /api/mdm/space-nodes/import | Excel导入 |
---
**文档版本**: v2.1
**最后更新**: 2026-04-26
> **TODO 项**:
> - SpaceNode 需增加 code 字段和唯一约束(当前代码中 code 字段可能缺失或不唯一)
> - 设备扩展字段attributes JSONB 中的设备信息)应统一到 Equipment 独立表管理,避免数据冗余