增加任务列表 mock 接口设计方案

This commit is contained in:
Tian jianyong 2025-10-09 15:35:26 +08:00
parent 317721d1a8
commit 4978c9ffaf
3 changed files with 852 additions and 1 deletions

View File

@ -1,5 +1,9 @@
## TODO Lists
### [2025-10-09]
- [ ] (功能)模拟无人车平台,返回任务列表
### [2025-09-05]
- [x] 模拟收到进出港航班信息,访问路由接口,获取路由信息并保存处理,同时发给前端显示路由
@ -12,7 +16,7 @@
- [ ] 任务指定
- [ ] 无人车上行接口
- [ ] 状态上报
- [x] 状态上报
- [ ] 视频流
- [ ] 任务列表
- [ ] 任务执行状态

View File

@ -0,0 +1,40 @@
# 关于SDK的要了解的几个问题
## webService服务器的IP地址和端口号
这个是数据中台服务的 IP 地址和端口?问谁可以获取到?
## 调用路由接口需要的信息,从哪些事件获取?
包括:
inRunway :进港跑道编号 35
outRunway出港跑道编号 34
contactCross联络道口 F1
seat目的机位 138
文档中找到:
【动态航班机位分配变更事件CRAFTSEAT】中有机位信息
【动态航班本站到达事件ARR】有到达信息
【推出开车事件(AXOT)】有进出港标志
【跑道号分配变更事件(RUNWAY)】有进出港标志和跑道信息
没有找到:联络道口信息
## 如何接收我们需要的事件?
receiveMessage()会接收所有的事件,还是会只返回我们需要的事件?
业务上是否需要进行接口申请和授权的流程?
## 如何使用接收数据接口MessageResult receiveMessage()接口
为了持续获取航班进出港事件,是不是需要持续调用这个接口?类似:
```java
public void receiveLoop() {
while (true) {
Message message = receive(); // 接收消息
if (message != null) {
// 用新线程处理消息
new Thread(() -> handleMessage(message)).start();
}
}
}
```

View File

@ -0,0 +1,807 @@
# 任务清单接口设计方案
**日期**: 2025-01-10
**版本**: v1.0
**目标**: 为无人车平台Mock服务增加任务清单接口
---
## 📋 需求背景
由无人车侧的平台mock提供一个单独的任务清单接口可以返回无人车所有的可执行任务。然后我们的应用请求这个接口选择某个任务下发给无人车去执行。
**核心概念**: 任务池/任务库 - 预定义的可执行任务模板清单
---
## 🎯 接口设计
### 接口信息
- **方法**: `GET /api/v1/missions/available`
- **认证**: Bearer Token (JWT)
- **响应**: `application/json; charset=utf-8`
- **功能**: 返回无人车平台的所有可执行任务清单
### 查询参数
| 参数名 | 类型 | 必填 | 描述 | 示例 |
|--------|------|------|------|------|
| missionType | string | 否 | 任务类型筛选 | PATROL_TRANSPORT, CARGO_TRANSPORT |
| priority | integer | 否 | 优先级筛选 | 1-5 |
| minBatteryLevel | integer | 否 | 最低电量要求 | 30 |
| limit | integer | 否 | 返回数量限制 | 10 |
| offset | integer | 否 | 分页偏移量 | 0 |
### 请求头
| Header | 必填 | 示例 | 说明 |
|--------|------|------|------|
| Authorization | 是 | Bearer eyJhbGciOi... | JWT 鉴权令牌 |
| Content-Type | 是 | application/json | 固定为 JSON |
| Accept | 否 | application/json | 建议显式声明 |
---
## 📦 数据结构设计
### 任务对象 (Mission)
```json
{
"missionId": "MISSION_TEMPLATE_001",
"missionName": "T1航站楼巡逻",
"missionType": "PATROL_TRANSPORT",
"description": "T1航站楼周边区域巡逻任务",
"priority": 3,
"estimatedDuration": 1800000,
"totalMileage": 2500.0,
"waypoints": [
{
"waypointId": "WP_001",
"latitude": 36.354068,
"longitude": 120.083410,
"waypointType": "START",
"name": "T1航站楼东侧",
"altitude": 10.5,
"estimatedArrivalTime": null
},
{
"waypointId": "WP_002",
"latitude": 36.355123,
"longitude": 120.084567,
"waypointType": "CHECKPOINT",
"name": "巡逻检查点A",
"altitude": 11.2,
"estimatedArrivalTime": null
},
{
"waypointId": "WP_003",
"latitude": 36.356789,
"longitude": 120.085234,
"waypointType": "END",
"name": "T1航站楼北侧",
"altitude": 10.8,
"estimatedArrivalTime": null
}
],
"requirements": {
"vehicleType": "UNMANNED",
"minBatteryLevel": 30,
"requiredSensors": ["GPS", "LIDAR", "CAMERA"],
"weatherConditions": "CLEAR | LIGHT_RAIN"
},
"metadata": {
"createdTime": 1736175000000,
"createdBy": "system",
"tags": ["patrol", "security", "routine"],
"zone": "TERMINAL_1"
}
}
```
### 字段说明
#### 基础信息
- `missionId` (string): 任务唯一标识
- `missionName` (string): 任务名称
- `missionType` (string): 任务类型
- `description` (string): 任务描述
- `priority` (integer): 优先级 1-5数字越大优先级越高
#### 时间和里程
- `estimatedDuration` (integer): 预计执行时长(毫秒)
- `totalMileage` (number): 预计总里程(米)
#### 路径点 (waypoints)
- `waypointId` (string): 路径点ID
- `latitude` (number): 纬度 (WGS84)
- `longitude` (number): 经度 (WGS84)
- `waypointType` (string): 路径点类型
- `START`: 起点
- `CHECKPOINT`: 检查点
- `WAYPOINT`: 普通路径点
- `END`: 终点
- `name` (string): 路径点名称
- `altitude` (number, 可选): 海拔高度(米)
- `estimatedArrivalTime` (integer, 可选): 预计到达时间毫秒UTC
#### 任务要求 (requirements)
- `vehicleType` (string): 车辆类型要求
- `minBatteryLevel` (integer): 最低电量要求 (0-100)
- `requiredSensors` (array): 必需的传感器列表
- `weatherConditions` (string): 可执行天气条件
#### 元数据 (metadata)
- `createdTime` (integer): 创建时间毫秒UTC
- `createdBy` (string): 创建者
- `tags` (array): 标签列表
- `zone` (string): 区域标识
---
## 📡 响应格式
### 成功响应 (200 OK)
```json
{
"code": 200,
"message": "success",
"timestamp": 1736175610000,
"data": {
"total": 5,
"missions": [
{
"missionId": "MISSION_TEMPLATE_001",
"missionName": "T1航站楼巡逻",
"missionType": "PATROL_TRANSPORT",
"description": "T1航站楼周边区域巡逻任务",
"priority": 3,
"estimatedDuration": 1800000,
"totalMileage": 2500.0,
"waypoints": [...],
"requirements": {...},
"metadata": {...}
},
{
"missionId": "MISSION_TEMPLATE_002",
"missionName": "货物运输-仓库A到登机口B3",
"missionType": "CARGO_TRANSPORT",
"priority": 4,
"estimatedDuration": 900000,
"totalMileage": 1200.0,
"waypoints": [...],
"requirements": {...},
"metadata": {...}
}
]
}
}
```
### 错误响应
#### 401 未授权
```json
{
"code": 401,
"message": "Unauthorized: Invalid or expired token",
"timestamp": 1736175610000,
"data": null
}
```
#### 400 参数错误
```json
{
"code": 400,
"message": "Invalid parameter: missionType must be one of [PATROL_TRANSPORT, CARGO_TRANSPORT, PASSENGER_TRANSPORT]",
"timestamp": 1736175610000,
"data": null
}
```
---
## 🗂️ 任务类型定义
### 标准任务类型
| 任务类型 | 代码 | 描述 |
|---------|------|------|
| 巡逻运输 | PATROL_TRANSPORT | 定期巡逻和安全检查 |
| 货物运输 | CARGO_TRANSPORT | 货物运输和配送 |
| 客运服务 | PASSENGER_TRANSPORT | 旅客运输服务 |
| 应急响应 | EMERGENCY_RESPONSE | 应急事件响应 |
| 维护检查 | MAINTENANCE_INSPECTION | 设施维护和检查 |
| 清洁作业 | CLEANING_OPERATION | 清洁和环境维护 |
---
## 🔧 实现方案
### 1. 数据存储结构
`mock_unmanned_vehicle.py` 中定义任务池:
```python
# 可执行任务清单(任务模板库)
AVAILABLE_MISSIONS = [
{
"missionId": "MISSION_TEMPLATE_001",
"missionName": "T1航站楼巡逻",
"missionType": "PATROL_TRANSPORT",
"description": "T1航站楼周边区域巡逻任务",
"priority": 3,
"estimatedDuration": 1800000,
"totalMileage": 2500.0,
"waypoints": [
{
"waypointId": "WP_T1_001",
"latitude": 36.354068,
"longitude": 120.083410,
"waypointType": "START",
"name": "T1航站楼东侧",
"altitude": 10.5
},
{
"waypointId": "WP_T1_002",
"latitude": 36.355123,
"longitude": 120.084567,
"waypointType": "CHECKPOINT",
"name": "巡逻检查点A",
"altitude": 11.2
},
{
"waypointId": "WP_T1_003",
"latitude": 36.356789,
"longitude": 120.085234,
"waypointType": "END",
"name": "T1航站楼北侧",
"altitude": 10.8
}
],
"requirements": {
"vehicleType": "UNMANNED",
"minBatteryLevel": 30,
"requiredSensors": ["GPS", "LIDAR", "CAMERA"],
"weatherConditions": "CLEAR | LIGHT_RAIN"
},
"metadata": {
"createdTime": 1736175000000,
"createdBy": "system",
"tags": ["patrol", "security", "routine"],
"zone": "TERMINAL_1"
}
},
{
"missionId": "MISSION_TEMPLATE_002",
"missionName": "货物运输-仓库A到登机口B3",
"missionType": "CARGO_TRANSPORT",
"description": "从货物仓库A运输至B3登机口",
"priority": 4,
"estimatedDuration": 900000,
"totalMileage": 1200.0,
"waypoints": [
{
"waypointId": "WP_CARGO_001",
"latitude": 36.352000,
"longitude": 120.082000,
"waypointType": "START",
"name": "货物仓库A",
"altitude": 9.5
},
{
"waypointId": "WP_CARGO_002",
"latitude": 36.353500,
"longitude": 120.083500,
"waypointType": "WAYPOINT",
"name": "运输通道中点",
"altitude": 10.0
},
{
"waypointId": "WP_CARGO_003",
"latitude": 36.355000,
"longitude": 120.085000,
"waypointType": "END",
"name": "B3登机口",
"altitude": 10.2
}
],
"requirements": {
"vehicleType": "UNMANNED",
"minBatteryLevel": 25,
"requiredSensors": ["GPS", "CAMERA"],
"weatherConditions": "CLEAR | LIGHT_RAIN | CLOUDY",
"maxCargoWeight": 500
},
"metadata": {
"createdTime": 1736175100000,
"createdBy": "system",
"tags": ["cargo", "logistics", "high-priority"],
"zone": "CARGO_AREA"
}
},
{
"missionId": "MISSION_TEMPLATE_003",
"missionName": "T2航站楼接驳",
"missionType": "PASSENGER_TRANSPORT",
"description": "T2航站楼旅客接驳服务",
"priority": 5,
"estimatedDuration": 600000,
"totalMileage": 800.0,
"waypoints": [
{
"waypointId": "WP_T2_001",
"latitude": 36.358000,
"longitude": 120.086000,
"waypointType": "START",
"name": "T2到达大厅",
"altitude": 11.0
},
{
"waypointId": "WP_T2_002",
"latitude": 36.359000,
"longitude": 120.087000,
"waypointType": "END",
"name": "T2出发大厅",
"altitude": 11.5
}
],
"requirements": {
"vehicleType": "UNMANNED",
"minBatteryLevel": 40,
"requiredSensors": ["GPS", "LIDAR", "CAMERA", "ULTRASONIC"],
"weatherConditions": "CLEAR | CLOUDY",
"maxPassengerCount": 4
},
"metadata": {
"createdTime": 1736175200000,
"createdBy": "system",
"tags": ["passenger", "shuttle", "high-priority"],
"zone": "TERMINAL_2"
}
},
{
"missionId": "MISSION_TEMPLATE_004",
"missionName": "跑道周边应急巡查",
"missionType": "EMERGENCY_RESPONSE",
"description": "跑道周边区域应急巡查任务",
"priority": 5,
"estimatedDuration": 1200000,
"totalMileage": 3500.0,
"waypoints": [
{
"waypointId": "WP_RUNWAY_001",
"latitude": 36.360000,
"longitude": 120.088000,
"waypointType": "START",
"name": "跑道17端",
"altitude": 8.0
},
{
"waypointId": "WP_RUNWAY_002",
"latitude": 36.365000,
"longitude": 120.090000,
"waypointType": "CHECKPOINT",
"name": "跑道中段检查点",
"altitude": 8.5
},
{
"waypointId": "WP_RUNWAY_003",
"latitude": 36.370000,
"longitude": 120.092000,
"waypointType": "END",
"name": "跑道35端",
"altitude": 9.0
}
],
"requirements": {
"vehicleType": "UNMANNED",
"minBatteryLevel": 50,
"requiredSensors": ["GPS", "LIDAR", "CAMERA", "THERMAL"],
"weatherConditions": "ANY",
"emergencyLightRequired": true
},
"metadata": {
"createdTime": 1736175300000,
"createdBy": "system",
"tags": ["emergency", "runway", "critical"],
"zone": "RUNWAY_AREA"
}
},
{
"missionId": "MISSION_TEMPLATE_005",
"missionName": "停机坪维护巡检",
"missionType": "MAINTENANCE_INSPECTION",
"description": "停机坪设施维护巡检",
"priority": 2,
"estimatedDuration": 2400000,
"totalMileage": 4200.0,
"waypoints": [
{
"waypointId": "WP_APRON_001",
"latitude": 36.351000,
"longitude": 120.081000,
"waypointType": "START",
"name": "停机坪A1",
"altitude": 10.0
},
{
"waypointId": "WP_APRON_002",
"latitude": 36.352500,
"longitude": 120.082500,
"waypointType": "CHECKPOINT",
"name": "停机坪A5",
"altitude": 10.2
},
{
"waypointId": "WP_APRON_003",
"latitude": 36.354000,
"longitude": 120.084000,
"waypointType": "CHECKPOINT",
"name": "停机坪B2",
"altitude": 10.5
},
{
"waypointId": "WP_APRON_004",
"latitude": 36.355500,
"longitude": 120.085500,
"waypointType": "END",
"name": "停机坪B8",
"altitude": 10.8
}
],
"requirements": {
"vehicleType": "UNMANNED",
"minBatteryLevel": 35,
"requiredSensors": ["GPS", "CAMERA", "INSPECTION_TOOLS"],
"weatherConditions": "CLEAR | CLOUDY"
},
"metadata": {
"createdTime": 1736175400000,
"createdBy": "system",
"tags": ["maintenance", "inspection", "routine"],
"zone": "APRON_AREA"
}
}
]
# 任务类型枚举
MISSION_TYPES = {
"PATROL_TRANSPORT": "巡逻运输",
"CARGO_TRANSPORT": "货物运输",
"PASSENGER_TRANSPORT": "客运服务",
"EMERGENCY_RESPONSE": "应急响应",
"MAINTENANCE_INSPECTION": "维护检查",
"CLEANING_OPERATION": "清洁作业"
}
```
### 2. 接口处理函数
```python
@app.route('/api/v1/missions/available', methods=['GET', 'OPTIONS'])
def get_available_missions():
"""
获取可执行任务清单
查询参数:
- missionType: 任务类型筛选
- priority: 优先级筛选
- minBatteryLevel: 最低电量要求
- limit: 返回数量限制
- offset: 分页偏移量
"""
# 处理 CORS 预检请求
if request.method == 'OPTIONS':
return '', 200
try:
# 获取查询参数
mission_type = request.args.get('missionType')
priority = request.args.get('priority', type=int)
min_battery = request.args.get('minBatteryLevel', type=int)
limit = request.args.get('limit', type=int, default=100)
offset = request.args.get('offset', type=int, default=0)
# 筛选任务
filtered_missions = AVAILABLE_MISSIONS.copy()
# 按任务类型筛选
if mission_type:
if mission_type not in MISSION_TYPES:
return jsonify({
"code": 400,
"message": f"Invalid missionType. Must be one of {list(MISSION_TYPES.keys())}",
"timestamp": int(time.time() * 1000),
"data": None
}), 400
filtered_missions = [m for m in filtered_missions if m["missionType"] == mission_type]
# 按优先级筛选
if priority is not None:
if priority < 1 or priority > 5:
return jsonify({
"code": 400,
"message": "Invalid priority. Must be between 1 and 5",
"timestamp": int(time.time() * 1000),
"data": None
}), 400
filtered_missions = [m for m in filtered_missions if m["priority"] == priority]
# 按电量要求筛选
if min_battery is not None:
if min_battery < 0 or min_battery > 100:
return jsonify({
"code": 400,
"message": "Invalid minBatteryLevel. Must be between 0 and 100",
"timestamp": int(time.time() * 1000),
"data": None
}), 400
filtered_missions = [
m for m in filtered_missions
if m["requirements"]["minBatteryLevel"] <= min_battery
]
# 分页处理
total = len(filtered_missions)
paginated_missions = filtered_missions[offset:offset + limit]
# 构造响应
response = {
"code": 200,
"message": "success",
"timestamp": int(time.time() * 1000),
"data": {
"total": total,
"limit": limit,
"offset": offset,
"missions": paginated_missions
}
}
logging.info(f"返回 {len(paginated_missions)} 个可执行任务 (总数: {total})")
return jsonify(response), 200
except Exception as e:
logging.error(f"获取任务清单失败: {str(e)}")
return jsonify({
"code": 500,
"message": f"Internal server error: {str(e)}",
"timestamp": int(time.time() * 1000),
"data": None
}), 500
```
### 3. CORS 配置
确保在文件中已有 CORS 配置:
```python
@app.after_request
def after_request(response):
"""添加 CORS 头"""
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization,Accept,X-Request-Id')
response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
return response
```
---
## 🧪 测试用例
### 1. 获取所有任务
```bash
curl -X GET "http://localhost:5000/api/v1/missions/available" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Accept: application/json"
```
### 2. 按任务类型筛选
```bash
curl -X GET "http://localhost:5000/api/v1/missions/available?missionType=CARGO_TRANSPORT" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Accept: application/json"
```
### 3. 按优先级筛选
```bash
curl -X GET "http://localhost:5000/api/v1/missions/available?priority=5" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Accept: application/json"
```
### 4. 按电量要求筛选
```bash
curl -X GET "http://localhost:5000/api/v1/missions/available?minBatteryLevel=50" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Accept: application/json"
```
### 5. 分页查询
```bash
curl -X GET "http://localhost:5000/api/v1/missions/available?limit=2&offset=0" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Accept: application/json"
```
### 6. 组合筛选
```bash
curl -X GET "http://localhost:5000/api/v1/missions/available?missionType=PATROL_TRANSPORT&priority=3&limit=10" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Accept: application/json"
```
---
## 📐 实施步骤
### Phase 1: 基础实现 ✅
1. 在 `mock_unmanned_vehicle.py` 中添加任务池数据结构约200行
2. 实现 `/api/v1/missions/available` 接口约80行
3. 添加基本的任务类型和优先级筛选
4. 测试接口基本功能
### Phase 2: 增强功能
1. 添加更多样化的任务模板(不同区域、不同类型)
2. 实现高级筛选(按区域、按标签、按天气条件等)
3. 添加任务搜索功能(按名称、描述关键字)
4. 优化分页性能
### Phase 3: 集成与扩展
1. 与任务下发接口集成(下一步需求)
2. 添加任务状态管理(可执行、进行中、已完成)
3. 实现任务动态更新机制
4. 添加任务执行统计和历史记录
---
## 🔗 与现有系统的集成
### 与 `/status` 接口的关系
- `/status` 接口:返回车辆当前正在执行的任务状态
- `/missions/available` 接口:返回可供选择的任务清单
### 数据一致性
任务模板中的字段结构应与 `missionContext` 保持一致:
```python
# 当任务被分配给车辆时,从任务模板创建任务实例
def assign_mission_to_vehicle(vehicle_id, mission_template_id):
template = get_mission_template(mission_template_id)
# 创建任务实例
mission_instance = {
"missionId": f"{template['missionId']}_INSTANCE_{timestamp}",
"missionType": template["missionType"],
"startTime": int(time.time() * 1000),
"estimatedEndTime": int(time.time() * 1000) + template["estimatedDuration"],
"progress": 0.0,
"totalMileage": template["totalMileage"],
"waypoints": [
{**wp, "status": "PENDING"}
for wp in template["waypoints"]
]
}
# 更新车辆数据
vehicle_data["mission_id"] = mission_instance["missionId"]
vehicle_data["mission_type"] = mission_instance["missionType"]
vehicle_data["mission_start_time"] = mission_instance["startTime"]
return mission_instance
```
---
## 📊 后续扩展方向
### 1. 任务下发接口
```
POST /api/v1/vehicles/{vehicleId}/missions
```
用于将选中的任务分配给指定车辆。
### 2. 任务管理接口
```
GET /api/v1/missions/{missionId} # 获取任务详情
PUT /api/v1/missions/{missionId} # 更新任务
DELETE /api/v1/missions/{missionId} # 删除任务
POST /api/v1/missions # 创建新任务模板
```
### 3. 任务执行控制
```
POST /api/v1/vehicles/{vehicleId}/missions/{missionId}/pause # 暂停任务
POST /api/v1/vehicles/{vehicleId}/missions/{missionId}/resume # 恢复任务
POST /api/v1/vehicles/{vehicleId}/missions/{missionId}/cancel # 取消任务
```
### 4. 任务统计与分析
```
GET /api/v1/missions/statistics # 任务统计信息
GET /api/v1/missions/history # 任务执行历史
```
---
## ✅ 验收标准
1. **接口可访问性**
- [ ] 接口正常响应 200 状态码
- [ ] 支持 CORS 跨域请求
- [ ] JWT 认证正常工作
2. **数据完整性**
- [ ] 每个任务包含所有必需字段
- [ ] 路径点数据完整且符合规范
- [ ] 任务类型和状态枚举值正确
3. **筛选功能**
- [ ] 按任务类型筛选正常
- [ ] 按优先级筛选正常
- [ ] 按电量要求筛选正常
- [ ] 多条件组合筛选正常
4. **分页功能**
- [ ] limit 和 offset 参数正常工作
- [ ] total 字段返回正确的总数
- [ ] 边界情况处理正确
5. **错误处理**
- [ ] 无效参数返回 400 错误
- [ ] 未授权访问返回 401 错误
- [ ] 服务器错误返回 500 错误
- [ ] 错误消息清晰易懂
6. **性能要求**
- [ ] 响应时间 < 500ms
- [ ] 支持并发请求
- [ ] 日志记录完整
---
## 📝 附录
### A. 坐标系统说明
所有坐标使用 **WGS84 坐标系**
- 纬度范围: -90 ~ 90
- 经度范围: -180 ~ 180
- 青岛胶东国际机场参考中心: (120.0834104, 36.35406879)
### B. 时间格式说明
所有时间戳使用 **毫秒级 UTC 时间**
- 格式: Unix timestamp (milliseconds)
- 示例: 1736175610000
- 转换: `int(time.time() * 1000)`
### C. 参考文档
- 通用无人车运行状态API: `doc/requirement/universal_autonomous_vehicle_api_min_required.md`
- Mock服务主文件: `tools/mock_unmanned_vehicle.py`
- 项目说明文档: `CLAUDE.md`
---
**文档结束**