添加其他逻辑

This commit is contained in:
靳中伟 2025-07-30 15:11:59 +08:00
parent de82ddf7cd
commit 1185f25fd3
132 changed files with 156158 additions and 65817 deletions

169
CLAUDE.md Normal file
View File

@ -0,0 +1,169 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
VWED Task Module is a FastAPI-based Python system for managing and executing task workflows for AMR (Autonomous Mobile Robot) scheduling systems. The system provides a low-code configuration tool that allows users to design and configure complex robot task flows through a visual interface.
## Development Commands
### Application Startup
```bash
# Primary method - run the main application
python app.py
# Alternative method - using uvicorn directly
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
```
### Database Operations
```bash
# Run database migrations
python scripts/run_migration.py
# Generate new migrations
python scripts/generate_migration.py
# Initialize database
python scripts/init_db.py
```
### Docker Deployment
```bash
# Build and run with Docker
docker build -t vwed-task:latest .
docker run -d -p 8000:8000 --name vwed-task-container vwed-task:latest
# Or use Docker Compose (recommended)
docker-compose up -d
```
## Architecture Overview
The system follows a layered architecture with clear separation of concerns:
### Core Layers
- **API Layer (`routes/`)**: FastAPI REST endpoints for client communication
- **Middleware Layer (`middlewares/`)**: Request logging, error handling, and cross-cutting concerns
- **Business Logic Layer (`services/`)**: Core business logic including task execution and scheduling
- **Data Layer (`data/`)**: ORM models, database session management, and data persistence
- **Component Layer (`components/`)**: Extensible component system for task workflow building
- **Configuration Layer (`config/`)**: System settings, database configuration, and error mappings
### Key Services
- **Task Execution Engine (`services/execution/`)**: Handles task lifecycle and execution
- `task_executor.py`: Main task execution controller
- `block_executor.py`: Individual task block execution
- `task_context.py`: Execution context and variable management
- `handlers/`: Specific handlers for different component types
- **Enhanced Scheduler (`services/enhanced_scheduler/`)**: High-performance async task scheduling
- **Intelligence Layer (`services/intelligence/`)**: AI-powered features (partial implementation)
### Data Models
Key models in `data/models/`:
- `taskdef.py`: Task definition and configuration
- `taskrecord.py`: Task execution records
- `blockrecord.py`: Individual block execution records
- `tasktemplate.py`: Reusable task templates
- `calldevice.py`: Device integration configurations
## Key Features
### Task Management
- Visual task flow designer with drag-and-drop interface
- Version control for task configurations
- Task templates for reusability
- Real-time execution monitoring
### Component System
Components are registered in `config/components/` and include:
- Foundation components (basic operations)
- Robot scheduling components
- HTTP request components
- Storage location management
- Progress tracking components
- Script execution components
### Device Integration
- Modbus protocol support (`modbusconfig.py`)
- Generic device calling interface (`calldevice.py`)
- Extensible communication protocols
## Development Guidelines
### Database Schema Changes
1. Modify models in `data/models/`
2. Generate migration: `python scripts/generate_migration.py`
3. Apply migration: `python scripts/run_migration.py`
### Adding New Components
1. Define component configuration in `config/components/`
2. Implement handler in `services/execution/handlers/`
3. Register component in the component system
### Task Execution Flow
1. Task definition stored in `taskdef` table
2. Execution creates `taskrecord` entry
3. Individual blocks create `blockrecord` entries
4. Context variables managed through `task_context.py`
5. Component handlers execute specific business logic
### Configuration Management
- Environment-specific settings in `config/settings.py`
- Database configuration in `config/database_config.py`
- Error messages centralized in `config/error_messages.py`
## Important Technical Details
### Async Task Scheduling
The system uses a custom enhanced scheduler (`services/enhanced_scheduler/`) that:
- Maintains worker pools for concurrent task execution
- Provides priority-based task queuing
- Handles task persistence and recovery
- Manages worker lifecycle
### Component Architecture
Components follow a registry pattern:
- Each component type has a handler class
- Handlers implement standard execution interface
- Components are configurable through JSON definitions
- Extensible for new component types
### Database Session Management
- Uses SQLAlchemy ORM with session management in `data/session.py`
- Supports both sync and async database operations
- Automatic connection pooling and cleanup
## Testing and Debugging
### Test Files
- Basic tests in `tests/` directory
- Test data and fixtures available
- Integration tests for API endpoints
### Logging
- Centralized logging configuration in `utils/logger.py`
- Application logs stored in `logs/` directory
- Structured logging for debugging task execution
### API Documentation
- Swagger UI available at `http://localhost:8000/docs`
- Comprehensive API documentation in `VWED任务模块接口文档/`
## Common Troubleshooting
### Database Issues
- Check database connection in `config/database_config.py`
- Verify database migrations are up to date
- Review logs in `logs/app.log`
### Task Execution Problems
- Monitor task status through API endpoints
- Check execution logs for specific error messages
- Verify component configurations are correct
### Performance Optimization
- Adjust scheduler worker counts in settings
- Monitor database connection pool usage
- Review task complexity and component efficiency

View File

@ -565,6 +565,9 @@ ws://your-domain/ws/storage-location-broadcast/{scene_id}
##### 心跳检测 ##### 心跳检测
支持两种格式的心跳检测:
**JSON格式心跳**
```json ```json
{ {
"type": "ping", "type": "ping",
@ -572,10 +575,105 @@ ws://your-domain/ws/storage-location-broadcast/{scene_id}
} }
``` ```
**字符串格式心跳:**
```
ping
```
#### 服务器消息格式 #### 服务器消息格式
与库位状态实时推送接口相同,参见上述文档。 与库位状态实时推送接口相同,参见上述文档。
##### 心跳响应
对于JSON格式心跳请求服务器返回JSON格式响应
```json
{
"type": "pong",
"timestamp": "2025-06-11T12:00:00.000Z"
}
```
对于字符串格式心跳请求,服务器返回字符串响应:
```
pong
```
#### 连接示例
##### JavaScript客户端示例
```javascript
// 建立WebSocket连接
const sceneId = "your-scene-id";
const wsUrl = `ws://localhost:8000/ws/storage-location-broadcast/${sceneId}`;
const websocket = new WebSocket(wsUrl);
// 连接建立
websocket.onopen = function(event) {
console.log("库位状态广播WebSocket连接已建立");
// 发送JSON格式心跳包
websocket.send(JSON.stringify({
type: "ping",
timestamp: new Date().toISOString()
}));
// 或者发送字符串格式心跳包
// websocket.send("ping");
};
// 接收消息
websocket.onmessage = function(event) {
try {
// 尝试解析JSON格式响应
const data = JSON.parse(event.data);
switch(data.type) {
case "storage_location_update":
console.log("库位状态更新:", data.data);
break;
case "storage_location_status_change":
console.log("库位状态变化:", data.layer_name, data.action);
break;
case "pong":
console.log("JSON格式心跳响应:", data.timestamp);
break;
case "error":
console.error("服务器错误:", data.message);
break;
}
} catch (e) {
// 处理字符串格式响应
if (event.data === "pong") {
console.log("字符串格式心跳响应: pong");
} else {
console.log("收到未知消息:", event.data);
}
}
};
// 定期发送心跳
setInterval(() => {
if (websocket.readyState === WebSocket.OPEN) {
// 可以选择JSON格式或字符串格式
websocket.send("ping"); // 字符串格式
// websocket.send(JSON.stringify({type: "ping", timestamp: new Date().toISOString()})); // JSON格式
}
}, 30000); // 每30秒发送一次心跳
// 连接关闭
websocket.onclose = function(event) {
console.log("库位状态广播WebSocket连接已关闭");
};
// 连接错误
websocket.onerror = function(error) {
console.error("库位状态广播WebSocket连接错误:", error);
};
```
#### 使用场景 #### 使用场景
1. **监控面板**:多个监控客户端同时监听库位状态变化 1. **监控面板**:多个监控客户端同时监听库位状态变化
@ -637,3 +735,4 @@ ws://your-domain/ws/storage-location-broadcast/{scene_id}
| --- | --- | --- | | --- | --- | --- |
| 1.0.0 | 2025-06-11 | 初始版本,支持任务执行结果实时推送和广播功能 | | 1.0.0 | 2025-06-11 | 初始版本,支持任务执行结果实时推送和广播功能 |
| 1.1.0 | 2025-06-11 | 新增库位状态实时推送和广播功能,支持多种过滤条件和状态变化通知 | | 1.1.0 | 2025-06-11 | 新增库位状态实时推送和广播功能,支持多种过滤条件和状态变化通知 |
| 1.2.0 | 2025-07-18 | 更新心跳检测机制所有WebSocket接口现在支持JSON和字符串两种格式的心跳检测 |

View File

@ -95,14 +95,12 @@
{ {
"station_name": "STATION-A-001", "station_name": "STATION-A-001",
"area_name": "一般存储区B", "area_name": "一般存储区B",
"max_layers": 2, "max_layers": 1,
"layers": [ "layers": [
{ {
"layer_name": "1-1" // 库位名称 "layer_name": "DSA_2_1_1" // 库位名称
},
{
"layer_name": "1-2" // 库位名称
} }
] ]
}, },
{ {
@ -111,32 +109,28 @@
"max_layers": 1, "max_layers": 1,
"layers": [ "layers": [
{ {
"layer_name": "2-1" //库位名称 "layer_name": "DSA_2_1_2" //库位名称
} }
] ]
}, },
{ {
"station_name": "STATION-B-004", "station_name": "STATION-B-004",
"area_name": "一般存储区C", "area_name": "一般存储区B",
"max_layers": 3, "max_layers": 1,
"layers": [ "layers": [
{ {
"layer_name": "4-1" //库位名称 "layer_name": "DSA_2_1_3" //库位名称
},
{
"layer_name": "4-2" // 库位名称
},
{
"layer_name": "4-3"// 库位名称
} }
] ]
}, },
{ {
"station_name": "STATION-B-003", "station_name": "STATION-B-003",
"area_name": "一般存储区B",
"max_layers": 1, "max_layers": 1,
"layers": [ "layers": [
{ {
"layer_name": "3-1" //库位名称 "layer_name": "DSA_2_1_4" //库位名称
} }
] ]
} }

View File

@ -0,0 +1,242 @@
# 外部任务接口文档
## 外部任务模块接口
### 1. 创建新任务 (create_new_task)
#### 接口描述
根据任务类型自动选择对应的任务模板并执行任务,用于外部系统调用创建和启动任务。
#### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/newTask`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| ReqCode | String | 是 | 请求唯一标识码 |
| SourceID | String | 否 | 来源ID |
| TargetID | String | 是 | 目标ID |
| TaskType | TaskTypeEnum | 是 | 任务类型 |
#### 任务类型枚举 (TaskTypeEnum)
| 枚举值 | 描述 | 模板ID |
|-------|------|-------|
| GG2MP | 高柜到MP | template_gg2mp_id |
| GGFK2MP | 高柜发库到MP | template_ggfk2mp_id |
| GT2MP | 高台到MP | 571985c1-cfa5-4186-8acd-6e3868a5e08c |
| GTFK2MP | 高台发库到MP | template_gtfk2mp_id |
| ZG2MP | 中柜到MP | template_zg2mp_id |
| QZ2MP | 清洗到MP | template_qz2mp_id |
| LG2MP | 料柜到MP | template_lg2mp_id |
| PHZ2MP | 配货站到MP | template_phz2mp_id |
| MP2GG | MP到高柜 | template_mp2gg_id |
| MP2GGFK | MP到高柜发库 | template_mp2ggfk_id |
| MP2GT | MP到高台 | template_mp2gt_id |
| MP2GTFK | MP到高台发库 | template_mp2gtfk_id |
| MP2ZG | MP到中柜 | template_mp2zg_id |
| MP2QZ | MP到清洗 | template_mp2qz_id |
| MP2LG | MP到料柜 | template_mp2lg_id |
| MP2PHZ | MP到配货站 | template_mp2phz_id |
#### 请求示例
```json
{
"ReqCode": "123e4567-e89b-12d3-a456-426614174000",
"SourceID": "WH-A-001",
"TargetID": "WH-B-002",
"TaskType": "GT2MP"
}
```
#### 响应参数
```json
{
"code": 0,
"reqCode": "123e4567-e89b-12d3-a456-426614174000",
"message": "成功",
"rowCount": 1
}
```
#### 错误响应
1. 不支持的任务类型时:
```json
{
"code": 400,
"reqCode": "123e4567-e89b-12d3-a456-426614174000",
"message": "不支持的任务类型: UNKNOWN_TYPE",
"rowCount": 0
}
```
2. 任务启动失败时:
```json
{
"code": 500,
"reqCode": "123e4567-e89b-12d3-a456-426614174000",
"message": "任务启动失败",
"rowCount": 0
}
```
3. 系统异常时:
```json
{
"code": 500,
"reqCode": "123e4567-e89b-12d3-a456-426614174000",
"message": "创建任务失败: [详细错误信息]",
"rowCount": 0
}
```
### 2. AGV调度任务 (GenAgvSchedulingTask)
#### 接口描述
用于生成AGV调度任务支持更丰富的调度参数配置包括位置路径、安全密钥验证等。
#### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/GenAgvSchedulingTask`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| ReqCode | String | 是 | 请求唯一标识码 |
| TaskTyp | String | 是 | 任务类型使用TaskTypeEnum中的值 |
| SecurityKey | String | 是 | 安全密钥 |
| Type | String | 是 | 类型标识 |
| TaskCode | String | 是 | 任务代码 |
| SubType | String | 是 | 子类型标识 |
| AreaPositonCode | String | 是 | 区域位置代码 |
| AreaPositonName | String | 是 | 区域位置名称 |
| PositionCodePath | Array<PositionCodePath> | 是 | 位置代码路径 |
| ClientCode | String | 否 | 客户端代码 |
| TokenCode | String | 否 | 令牌代码 |
#### PositionCodePath 参数结构
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| PositionCode | String | 是 | 位置代码 |
| Type | String | 是 | 类型 |
#### 请求示例
```json
{
"ReqCode": "a98334b940af48328290389a64b71bcc",
"TaskTyp": "MP2GGFK",
"SecurityKey": "2JN4YW5IJSQRXDEPBT3YQKEGJMT2GS1X",
"Type": "1",
"TaskCode": "d074b178e9c54a72a484a782d8955aa3",
"SubType": "3",
"AreaPositonCode": "1-1",
"AreaPositonName": "1-1,ZK",
"PositionCodePath": [
{
"PositionCode": "AP190",
"Type": "00"
},
{
"PositionCode": "AP313",
"Type": "04"
}
],
"ClientCode": "",
"TokenCode": ""
}
```
#### 响应参数
```json
{
"code": 0,
"reqCode": "a98334b940af48328290389a64b71bcc",
"message": "成功",
"rowCount": 0
}
```
#### 错误响应
1. 安全密钥为空时:
```json
{
"code": 400,
"reqCode": "a98334b940af48328290389a64b71bcc",
"message": "安全密钥不能为空",
"rowCount": 0
}
```
2. 不支持的任务类型时:
```json
{
"code": 400,
"reqCode": "a98334b940af48328290389a64b71bcc",
"message": "不支持的任务类型: UNKNOWN_TYPE",
"rowCount": 0
}
```
3. 任务启动失败时:
```json
{
"code": 500,
"reqCode": "a98334b940af48328290389a64b71bcc",
"message": "任务启动失败",
"rowCount": 0
}
```
4. 系统异常时:
```json
{
"code": 500,
"reqCode": "a98334b940af48328290389a64b71bcc",
"message": "创建任务失败: [详细错误信息]",
"rowCount": 0
}
```
## 通用说明
### 响应码说明
| 响应码 | 描述 |
|-------|------|
| 0 | 成功 |
| 400 | 请求参数错误 |
| 404 | 资源不存在 |
| 409 | 冲突(如任务已在运行) |
| 500 | 服务器内部错误 |
### 安全说明
1. 所有接口都需要提供有效的请求标识码(ReqCode)
2. AGV调度任务接口需要提供有效的安全密钥(SecurityKey)
3. 系统会记录所有请求的客户端信息和来源
4. 建议在生产环境中启用HTTPS
### 调用注意事项
1. 请求标识码(ReqCode)应保证全局唯一性
2. 任务类型必须在支持的枚举范围内
3. 系统会根据任务类型自动选择对应的任务模板
4. 位置代码路径用于指定AGV的行驶路径
5. 系统支持并发调用,但相同设备的相同任务可能有限制
### 监控和日志
1. 所有接口调用都会记录详细日志
2. 支持通过ReqCode追踪任务执行状态
3. 错误信息会包含足够的上下文用于排查问题
4. 建议定期监控接口响应时间和成功率

View File

@ -22,7 +22,13 @@
### 1. 获取库位列表 (GET /api/vwed-operate-point/list) ### 1. 获取库位列表 (GET /api/vwed-operate-point/list)
获取库位列表,支持多种筛选条件和分页查询。 获取库位列表,支持多种筛选条件、排序功能和分页查询。
**排序功能说明:**
- `layer_name_sort`:支持按层名称排序,如 GSD_1_1_1 格式,先按数字部分排序,然后按字母部分排序
- `station_name_sort`:支持按站点名称排序,如 AP1 格式,先按数字部分排序,然后按字母部分排序
- 排序规则:首先按数字排序,如果数字都一样的话,按照字母排序
- 可以同时使用两种排序,优先按 layer_name_sort 排序,然后按 station_name_sort 排序
#### 请求参数 #### 请求参数
@ -38,6 +44,8 @@
| is_empty_tray | boolean | 否 | 是否空托盘 | | is_empty_tray | boolean | 否 | 是否空托盘 |
| include_operate_point_info | boolean | 否 | 是否包含动作点信息默认true | | include_operate_point_info | boolean | 否 | 是否包含动作点信息默认true |
| include_extended_fields | boolean | 否 | 是否包含扩展字段默认true | | include_extended_fields | boolean | 否 | 是否包含扩展字段默认true |
| layer_name_sort | boolean | 否 | 层名称排序true升序默认、false降序 |
| station_name_sort | boolean | 否 | 站点名称排序true升序默认、false降序 |
| page | integer | 否 | 页码默认1 | | page | integer | 否 | 页码默认1 |
| page_size | integer | 否 | 每页数量默认20最大100 | | page_size | integer | 否 | 每页数量默认20最大100 |
@ -152,10 +160,23 @@
#### 调用示例 #### 调用示例
**基本查询:**
```bash ```bash
GET /api/vwed-operate-point/list?scene_id=scene-001&is_occupied=false&page=1&page_size=20 GET /api/vwed-operate-point/list?scene_id=scene-001&is_occupied=false&page=1&page_size=20
``` ```
**带排序的查询:**
```bash
# 按层名称升序排序
GET /api/vwed-operate-point/list?layer_name_sort=true&page=1&page_size=20
# 按站点名称降序排序
GET /api/vwed-operate-point/list?station_name_sort=false&page=1&page_size=20
# 组合排序:先按层名称升序,再按站点名称降序
GET /api/vwed-operate-point/list?layer_name_sort=true&station_name_sort=false&page=1&page_size=20
```
--- ---
### 2. 更新库位状态 (PUT /api/vwed-operate-point/status) ### 2. 更新库位状态 (PUT /api/vwed-operate-point/status)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -53,25 +53,6 @@
} }
] ]
}, },
{
"name": "preBinTask",
"type": "String",
"label": "预置binTask",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "param",
"type": "String",
"label": "参数",
"description": "",
"required": false,
"defaultValue": null,
"options": []
}
]
},
{ {
"name": "pick", "name": "pick",
"type": "String", "type": "String",
@ -100,25 +81,6 @@
} }
] ]
}, },
{
"name": "JackHeight",
"type": "String",
"label": "设置顶升高度 JackHeight",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "targetHeight",
"type": "Double",
"label": "目标高度",
"description": "",
"required": true,
"defaultValue": null,
"options": []
}
]
},
{ {
"name": "drop", "name": "drop",
"type": "String", "type": "String",
@ -137,257 +99,6 @@
"options": [] "options": []
} }
] ]
},
{
"name": "Wait",
"type": "String",
"label": "等待 Wait",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "wait_time",
"type": "Integer",
"label": "等待时间(ms)",
"description": "",
"required": true,
"defaultValue": null,
"options": []
}
]
},
{
"name": "ForkLoad",
"type": "String",
"label": "叉车取货 ForkLoad",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "start_height",
"type": "Double",
"label": "起始高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "end_height",
"type": "Double",
"label": "结束高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "inspired_unique",
"type": "Boolean",
"label": "启用识别",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "unique_file",
"type": "String",
"label": "识别文件",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "special_height",
"type": "Double",
"label": "识别后高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
}
]
},
{
"name": "ForkUnload",
"type": "String",
"label": "降低叉齿 ForkUnload",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "start_height",
"type": "Double",
"label": "起始高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "end_height",
"type": "Double",
"label": "结束高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "inspired_unique",
"type": "Boolean",
"label": "启用识别",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "unique_file",
"type": "String",
"label": "识别文件",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "special_height",
"type": "Double",
"label": "识别后高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
}
]
},
{
"name": "ForkHeight",
"type": "String",
"label": "边走边升降叉齿 ForkHeight",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "start_height",
"type": "Double",
"label": "起始高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "fork_height",
"type": "Double",
"label": "货叉行走过程中举升高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
},
{
"name": "end_height",
"type": "Double",
"label": "结束高度",
"description": "",
"required": false,
"defaultValue": null,
"options": []
}
]
},
{
"name": "CustomCommand",
"type": "String",
"label": "自定义指令",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "command",
"type": "String",
"label": "指令方式",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "operation_name",
"type": "String",
"label": "操作名",
"description": "",
"required": true,
"defaultValue": null,
"options": []
},
{
"name": "param",
"type": "JSONArray",
"label": "属性",
"description": "",
"required": false,
"defaultValue": null,
"options": []
}
]
}
]
},
{
"name": "syspy/setDO.py",
"type": "String",
"label": "syspy/setDO.py",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "DO",
"type": "JSONArray",
"label": "设置DO",
"description": "",
"required": true,
"defaultValue": null,
"options": []
}
]
},
{
"name": "syspy/waitDO.py",
"type": "String",
"label": "syspy/waitDO.py",
"description": "",
"required": false,
"defaultValue": null,
"options": [
{
"name": "DO",
"type": "JSONArray",
"label": "设置DO",
"description": "",
"required": true,
"defaultValue": null,
"options": []
},
{
"name": "timeout",
"type": "Integer",
"label": "超时时间(秒)",
"description": "",
"required": false,
"defaultValue": null,
"options": []
}
]
} }
] ]
} }

View File

@ -564,7 +564,7 @@
"label": "加锁者", "label": "加锁者",
"description": "", "description": "",
"required": false, "required": false,
"defaultValue": true, "defaultValue": null,
"options": [] "options": []
}, },
{ {
@ -645,7 +645,7 @@
"label": "解锁者", "label": "解锁者",
"description": "", "description": "",
"required": false, "required": false,
"defaultValue": true, "defaultValue": null,
"options": [] "options": []
} }
], ],

View File

@ -326,8 +326,8 @@ class BaseConfig(BaseSettings):
TASK_EXPORT_IV: str = Field(default="vwed1234task5678", env="TASK_EXPORT_IV") # 初始化向量 TASK_EXPORT_IV: str = Field(default="vwed1234task5678", env="TASK_EXPORT_IV") # 初始化向量
# 增强版任务调度器配置 # 增强版任务调度器配置
TASK_SCHEDULER_MIN_WORKER_COUNT: int = 15 # 最小工作线程数 TASK_SCHEDULER_MIN_WORKER_COUNT: int = 100 # 最小工作线程数
TASK_SCHEDULER_MAX_WORKER_COUNT: int = 30 # 最大工作线程数 TASK_SCHEDULER_MAX_WORKER_COUNT: int = 150 # 最大工作线程数
TASK_SCHEDULER_QUEUE_COUNT: int = 3 # 队列数量 TASK_SCHEDULER_QUEUE_COUNT: int = 3 # 队列数量
TASK_SCHEDULER_QUEUE_THRESHOLD_PERCENTILES: List[float] = [0.1, 0.3, 1.0] # 队列阈值百分比配置 TASK_SCHEDULER_QUEUE_THRESHOLD_PERCENTILES: List[float] = [0.1, 0.3, 1.0] # 队列阈值百分比配置
TASK_SCHEDULER_WORKER_RATIOS: List[float] = [0.6, 0.3, 0.1] # 工作线程分配比例 TASK_SCHEDULER_WORKER_RATIOS: List[float] = [0.6, 0.3, 0.1] # 工作线程分配比例
@ -341,13 +341,10 @@ class BaseConfig(BaseSettings):
TASK_SCHEDULER_CPU_THRESHOLD: float = 80.0 # CPU使用率阈值百分比 TASK_SCHEDULER_CPU_THRESHOLD: float = 80.0 # CPU使用率阈值百分比
TASK_SCHEDULER_MEMORY_THRESHOLD: float = 80.0 # 内存使用率阈值(百分比) TASK_SCHEDULER_MEMORY_THRESHOLD: float = 80.0 # 内存使用率阈值(百分比)
TASK_SCHEDULER_AUTO_SCALE_INTERVAL: int = 120 # 自动扩缩容间隔(秒) TASK_SCHEDULER_AUTO_SCALE_INTERVAL: int = 120 # 自动扩缩容间隔(秒)
TASK_SCHEDULER_WORKER_HEARTBEAT_INTERVAL: int = 120 # 心跳间隔(秒) TASK_SCHEDULER_WORKER_HEARTBEAT_INTERVAL: int = 1200 # 心跳间隔(秒)
# 告警同步配置 # 告警同步配置
ALERT_SYNC_ENABLED: bool = Field(default=True, env="ALERT_SYNC_ENABLED") # 是否启用告警同步 ALERT_SYNC_ENABLED: bool = Field(default=True, env="ALERT_SYNC_ENABLED") # 是否启用告警同步
ALERT_SYNC_HOST: str = Field(default="192.168.189.80", env="ALERT_SYNC_HOST") # 主系统IP
ALERT_SYNC_PORT: int = Field(default=8080, env="ALERT_SYNC_PORT") # 主系统端口
ALERT_SYNC_API_PATH: str = Field(default="/jeecg-boot/warning", env="ALERT_SYNC_API_PATH") # 告警API路径
ALERT_SYNC_TIMEOUT: int = Field(default=10, env="ALERT_SYNC_TIMEOUT") # 请求超时时间(秒) ALERT_SYNC_TIMEOUT: int = Field(default=10, env="ALERT_SYNC_TIMEOUT") # 请求超时时间(秒)
ALERT_SYNC_RETRY_COUNT: int = Field(default=3, env="ALERT_SYNC_RETRY_COUNT") # 重试次数 ALERT_SYNC_RETRY_COUNT: int = Field(default=3, env="ALERT_SYNC_RETRY_COUNT") # 重试次数
ALERT_SYNC_RETRY_DELAY: int = Field(default=1, env="ALERT_SYNC_RETRY_DELAY") # 重试延迟(秒) ALERT_SYNC_RETRY_DELAY: int = Field(default=1, env="ALERT_SYNC_RETRY_DELAY") # 重试延迟(秒)
@ -365,6 +362,14 @@ class BaseConfig(BaseSettings):
MAP_GENERAL_STORAGE_CAPACITY_PER_POINT: int = Field(default=15, env="MAP_GENERAL_STORAGE_CAPACITY_PER_POINT") # 一般库区每个动作点增加的容量 MAP_GENERAL_STORAGE_CAPACITY_PER_POINT: int = Field(default=15, env="MAP_GENERAL_STORAGE_CAPACITY_PER_POINT") # 一般库区每个动作点增加的容量
MAP_GENERAL_STORAGE_LAYER_MULTIPLIER: float = Field(default=1.2, env="MAP_GENERAL_STORAGE_LAYER_MULTIPLIER") # 一般库区分层倍数 MAP_GENERAL_STORAGE_LAYER_MULTIPLIER: float = Field(default=1.2, env="MAP_GENERAL_STORAGE_LAYER_MULTIPLIER") # 一般库区分层倍数
# 库位获取队列配置
STORAGE_QUEUE_MAX_WORKERS: int = Field(default=10, env="STORAGE_QUEUE_MAX_WORKERS") # 队列工作者数量
STORAGE_QUEUE_MAX_SIZE: int = Field(default=2000, env="STORAGE_QUEUE_MAX_SIZE") # 队列最大数量
STORAGE_QUEUE_ENABLE_TIMEOUT: bool = Field(default=False, env="STORAGE_QUEUE_ENABLE_TIMEOUT") # 是否启用超时
STORAGE_QUEUE_DEFAULT_TIMEOUT: int = Field(default=3600, env="STORAGE_QUEUE_DEFAULT_TIMEOUT") # 默认超时时间(秒),仅在启用超时时生效
STORAGE_QUEUE_CLEANUP_INTERVAL: int = Field(default=300, env="STORAGE_QUEUE_CLEANUP_INTERVAL") # 清理已完成请求的间隔(秒)
STORAGE_QUEUE_COMPLETED_REQUEST_TTL: int = Field(default=3600, env="STORAGE_QUEUE_COMPLETED_REQUEST_TTL") # 已完成请求保留时间(秒)
@property @property
def DATABASE_URL(self) -> str: def DATABASE_URL(self) -> str:
"""构建数据库连接URL""" """构建数据库连接URL"""
@ -396,10 +401,10 @@ class BaseConfig(BaseSettings):
return f"redis://:{encoded_password}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" return f"redis://:{encoded_password}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
@property # @property
def ALERT_SYNC_URL(self) -> str: # def ALERT_SYNC_URL(self) -> str:
"""构建告警同步URL""" # """构建告警同步URL"""
return f"http://{self.ALERT_SYNC_HOST}:{self.ALERT_SYNC_PORT}{self.ALERT_SYNC_API_PATH}" # return f"http://{self.ALERT_SYNC_HOST}:{self.ALERT_SYNC_PORT}{self.ALERT_SYNC_API_PATH}"
# 更新为Pydantic v2的配置方式 # 更新为Pydantic v2的配置方式
model_config = { model_config = {
@ -424,6 +429,11 @@ class DevelopmentConfig(BaseConfig):
MAP_GENERAL_STORAGE_BASE_CAPACITY: int = 15 MAP_GENERAL_STORAGE_BASE_CAPACITY: int = 15
MAP_GENERAL_STORAGE_CAPACITY_PER_POINT: int = 8 MAP_GENERAL_STORAGE_CAPACITY_PER_POINT: int = 8
# 开发环境队列配置
# STORAGE_QUEUE_MAX_WORKERS: int = 5
# STORAGE_QUEUE_MAX_SIZE: int = 500
# STORAGE_QUEUE_ENABLE_TIMEOUT: bool = False # 开发环境也不启用超时
# 根据环境变量选择配置 # 根据环境变量选择配置
def get_config(): def get_config():

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

121225
logs/app.log

File diff suppressed because it is too large Load Diff

96521
logs/app.log.1 Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -19,6 +19,7 @@ from routes.modbus_config_api import router as modbus_config_router
from routes.websocket_api import router as websocket_router from routes.websocket_api import router as websocket_router
from routes.map_data_api import router as map_data_router from routes.map_data_api import router as map_data_router
from routes.operate_point_api import router as operate_point_router from routes.operate_point_api import router as operate_point_router
from routes.external_task_api import router as external_task_router
# 路由列表,按照注册顺序排列 # 路由列表,按照注册顺序排列
routers = [ routers = [
@ -33,7 +34,8 @@ routers = [
modbus_config_router, modbus_config_router,
websocket_router, websocket_router,
map_data_router, map_data_router,
operate_point_router operate_point_router,
external_task_router
] ]
def register_routers(app): def register_routers(app):

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

383
routes/external_task_api.py Normal file
View File

@ -0,0 +1,383 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
外部任务接口API模块
提供外部系统调用的任务创建接口
"""
import json
from typing import Dict, Any
from fastapi import APIRouter, Body, Request
from routes.model.external_task_model import ExternalTaskRequest, ExternalTaskResponse, TaskTypeEnum, GenAgvSchedulingTaskRequest
from routes.model.task_edit_model import TaskEditRunRequest, TaskInputParamNew, InputParamType
from services.task_edit_service import TaskEditService
from routes.common_api import format_response, error_response
from utils.logger import get_logger
from data.enum.task_record_enum import SourceType
from config.tf_api_config import TF_API_TOKEN
# tf_api_config = get_tf_api_config()
# 创建路由
router = APIRouter(
prefix="",
tags=["外部任务接口"]
)
# 设置日志
logger = get_logger("app.external_task_api")
# 任务类型到模板ID的映射
TASK_TYPE_TEMPLATE_MAPPING = {
TaskTypeEnum.GG2MP: "template_gg2mp_id", # 高柜到MP模板ID
TaskTypeEnum.GGFK2MP: "template_ggfk2mp_id", # 高柜发库到MP模板ID
TaskTypeEnum.GT2MP: "571985c1-cfa5-4186-8acd-6e3868a5e08c", # 高台到MP模板ID
TaskTypeEnum.GTFK2MP: "template_gtfk2mp_id", # 高台发库到MP模板ID
TaskTypeEnum.ZG2MP: "template_zg2mp_id", # 中柜到MP模板ID
TaskTypeEnum.QZ2MP: "template_qz2mp_id", # 清洗到MP模板ID
TaskTypeEnum.LG2MP: "template_lg2mp_id", # 料柜到MP模板ID
TaskTypeEnum.PHZ2MP: "template_phz2mp_id", # 配货站到MP模板ID
TaskTypeEnum.MP2GG: "template_mp2gg_id", # MP到高柜模板ID
TaskTypeEnum.MP2GGFK: "571985c1-cfa5-4186-8acd-6e3868a5e08c", # MP到高柜发库模板ID
TaskTypeEnum.MP2GT: "template_mp2gt_id", # MP到高台模板ID
TaskTypeEnum.MP2GTFK: "template_mp2gtfk_id", # MP到高台发库模板ID
TaskTypeEnum.MP2ZG: "template_mp2zg_id", # MP到中柜模板ID
TaskTypeEnum.MP2QZ: "template_mp2qz_id", # MP到清洗模板ID
TaskTypeEnum.MP2LG: "template_mp2lg_id", # MP到料柜模板ID
TaskTypeEnum.MP2PHZ: "template_mp2phz_id", # MP到配货站模板ID
}
@router.post("/newTask")
async def create_new_task(request: Request, task_request: ExternalTaskRequest = Body(...)):
"""
创建新任务接口
根据任务类型自动选择对应的任务模板并执行任务
Args:
task_request: 外部任务创建请求包含ReqCodeSourceIDTargetIDTaskType
Returns:
ExternalTaskResponse: 包含codereqCodemessagerowCount的响应
"""
try:
logger.info(f"收到外部任务创建请求: ReqCode={task_request.ReqCode}, TaskType={task_request.TaskType}")
# 根据任务类型获取对应的模板ID
template_id = TASK_TYPE_TEMPLATE_MAPPING.get(task_request.TaskType)
# print("template_id::::", template_id, "==========")
if not template_id:
logger.error(f"不支持的任务类型: {task_request.TaskType}")
return ExternalTaskResponse(
code=400,
reqCode=task_request.ReqCode,
message=f"不支持的任务类型: {task_request.TaskType}",
rowCount=0
)
# 构造任务运行参数
task_params = []
# 添加SourceID参数如果提供
if task_request.SourceID:
task_params.append(TaskInputParamNew(
name="sourceId",
type=InputParamType.STRING,
label="来源ID",
required=False,
defaultValue=task_request.SourceID,
remark="外部接口传入的来源ID"
))
# 添加TargetID参数
task_params.append(TaskInputParamNew(
name="targetId",
type=InputParamType.STRING,
label="目标ID",
required=True,
defaultValue=task_request.TargetID,
remark="外部接口传入的目标ID"
))
# 添加ReqCode参数
task_params.append(TaskInputParamNew(
name="reqCode",
type=InputParamType.STRING,
label="请求标识码",
required=True,
defaultValue=task_request.ReqCode,
remark="外部接口传入的请求唯一标识码"
))
# 构造任务执行请求
run_request = TaskEditRunRequest(
taskId=template_id,
params=task_params,
source_type=SourceType.SYSTEM_SCHEDULING, # 第三方系统
source_system="EXTERNAL_API", # 外部接口系统标识
source_device=request.client.host if request.client else "unknown", # 使用客户端IP作为设备标识
use_modbus=False,
modbus_timeout=5000
)
# 获取客户端信息
client_ip = request.client.host if request.client else None
user_agent = request.headers.get("user-agent", "")
tf_api_token = TF_API_TOKEN
client_info = {
"user_agent": user_agent,
"headers": dict(request.headers),
"method": request.method,
"url": str(request.url)
}
client_info_str = json.dumps(client_info, ensure_ascii=False)
# print("tf_api_config::::::::::::", tf_api_token)
# print("run_request:::::::::", run_request)
# print("client_ip:::::::::", client_ip)
# print("client_info:::::::::", client_info)
# print("client_info_str:::::::::", client_info_str)
# print("tf_api_token:::::::::", tf_api_token)
# 调用任务执行服务
result = await TaskEditService.run_task(
run_request,
client_ip=client_ip,
client_info=client_info_str,
tf_api_token=tf_api_token
)
if result is None:
logger.error(f"任务启动失败: ReqCode={task_request.ReqCode}")
return ExternalTaskResponse(
code=500,
reqCode=task_request.ReqCode,
message="任务启动失败",
rowCount=0
)
if not result.get("success", False):
logger.error(f"任务启动失败: {result.get('message')}, ReqCode={task_request.ReqCode}")
return ExternalTaskResponse(
code=500,
reqCode=task_request.ReqCode,
message=result.get("message", "任务启动失败"),
rowCount=0
)
logger.info(f"任务启动成功: ReqCode={task_request.ReqCode}, TaskRecordId={result.get('taskRecordId')}")
return ExternalTaskResponse(
code=0,
reqCode=task_request.ReqCode,
message="成功",
rowCount=1
)
except Exception as e:
logger.error(f"创建外部任务异常: {str(e)}, ReqCode={task_request.ReqCode}")
return ExternalTaskResponse(
code=500,
reqCode=task_request.ReqCode,
message=f"创建任务失败: {str(e)}",
rowCount=0
)
@router.post("/GenAgvSchedulingTask")
async def gen_agv_scheduling_task(request: Request, task_request: GenAgvSchedulingTaskRequest = Body(...)):
"""
AGV调度任务接口
用于生成AGV调度任务
Args:
task_request: AGV调度任务请求包含ReqCodeTaskTypSecurityKey等参数
Returns:
ExternalTaskResponse: 包含codereqCodemessagerowCount的响应
"""
try:
logger.info(f"收到AGV调度任务请求: ReqCode={task_request.ReqCode}, TaskTyp={task_request.TaskTyp}")
# # 验证安全密钥(可根据实际需求实现验证逻辑)
# if not task_request.SecurityKey:
# logger.error(f"安全密钥不能为空: ReqCode={task_request.ReqCode}")
# return ExternalTaskResponse(
# code=400,
# reqCode=task_request.ReqCode,
# message="安全密钥不能为空",
# rowCount=0GenAgvSchedulingTaskRequest
# )
# 根据任务类型获取对应的模板ID
template_id = TASK_TYPE_TEMPLATE_MAPPING.get(task_request.TaskTyp)
# if not template_id:
# logger.error(f"不支持的任务类型: {task_request.TaskTyp}, ReqCode={task_request.ReqCode}")
# return ExternalTaskResponse(
# code=400,
# reqCode=task_request.ReqCode,
# message=f"不支持的任务类型: {task_request.TaskTyp}",
# rowCount=0
# )
# 构造任务运行参数
task_params = []
# 添加任务代码参数
task_params.append(TaskInputParamNew(
name="taskCode",
type=InputParamType.STRING,
label="任务代码",
required=True,
defaultValue=task_request.TaskCode,
remark="AGV调度任务代码"
))
# 添加类型参数
task_params.append(TaskInputParamNew(
name="type",
type=InputParamType.STRING,
label="类型",
required=True,
defaultValue=task_request.Type,
remark="任务类型标识"
))
# 添加子类型参数
task_params.append(TaskInputParamNew(
name="subType",
type=InputParamType.STRING,
label="子类型",
required=True,
defaultValue=task_request.SubType,
remark="任务子类型标识"
))
# 添加区域位置代码参数
task_params.append(TaskInputParamNew(
name="areaPositonCode",
type=InputParamType.STRING,
label="区域位置代码",
required=True,
defaultValue=task_request.AreaPositonCode,
remark="区域位置代码"
))
# 添加区域位置名称参数
task_params.append(TaskInputParamNew(
name="areaPositonName",
type=InputParamType.STRING,
label="区域位置名称",
required=True,
defaultValue=task_request.AreaPositonName,
remark="区域位置名称"
))
# 添加位置代码路径参数转为JSON字符串
position_path_json = json.dumps([path.dict() for path in task_request.PositionCodePath], ensure_ascii=False)
task_params.append(TaskInputParamNew(
name="positionCodePath",
type=InputParamType.STRING,
label="位置代码路径",
required=True,
defaultValue=position_path_json,
remark="位置代码路径JSON数组"
))
# 添加客户端代码参数(如果提供)
if task_request.ClientCode:
task_params.append(TaskInputParamNew(
name="clientCode",
type=InputParamType.STRING,
label="客户端代码",
required=False,
defaultValue=task_request.ClientCode,
remark="客户端代码"
))
# 添加令牌代码参数(如果提供)
if task_request.TokenCode:
task_params.append(TaskInputParamNew(
name="tokenCode",
type=InputParamType.STRING,
label="令牌代码",
required=False,
defaultValue=task_request.TokenCode,
remark="令牌代码"
))
# 添加ReqCode参数
task_params.append(TaskInputParamNew(
name="reqCode",
type=InputParamType.STRING,
label="请求标识码",
required=True,
defaultValue=task_request.ReqCode,
remark="请求唯一标识码"
))
# 构造任务执行请求
run_request = TaskEditRunRequest(
taskId=template_id,
params=task_params,
source_type=SourceType.SYSTEM_SCHEDULING, # 第三方系统
source_system="AGV_SCHEDULING", # AGV调度系统标识
source_device=request.client.host if request.client else "unknown", # 使用客户端IP作为设备标识
use_modbus=False,
modbus_timeout=5000
)
# 获取客户端信息
client_ip = request.client.host if request.client else None
user_agent = request.headers.get("user-agent", "")
tf_api_token = TF_API_TOKEN
client_info = {
"user_agent": user_agent,
"headers": dict(request.headers),
"method": request.method,
"url": str(request.url)
}
client_info_str = json.dumps(client_info, ensure_ascii=False)
# 调用任务执行服务
result = await TaskEditService.run_task(
run_request,
client_ip=client_ip,
client_info=client_info_str,
tf_api_token=tf_api_token
)
if result is None:
logger.error(f"AGV调度任务启动失败: ReqCode={task_request.ReqCode}")
return ExternalTaskResponse(
code=500,
reqCode=task_request.ReqCode,
message="任务启动失败",
rowCount=0
)
if not result.get("success", False):
logger.error(f"AGV调度任务启动失败: {result.get('message')}, ReqCode={task_request.ReqCode}")
return ExternalTaskResponse(
code=500,
reqCode=task_request.ReqCode,
message=result.get("message", "任务启动失败"),
rowCount=0
)
logger.info(f"AGV调度任务启动成功: ReqCode={task_request.ReqCode}, TaskRecordId={result.get('taskRecordId')}")
return ExternalTaskResponse(
code=0,
reqCode=task_request.ReqCode,
message="成功",
rowCount=0
)
except Exception as e:
logger.error(f"创建AGV调度任务异常: {str(e)}, ReqCode={task_request.ReqCode}")
return ExternalTaskResponse(
code=500,
reqCode=task_request.ReqCode,
message=f"创建任务失败: {str(e)}",
rowCount=0
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
外部任务接口模型模块
包含外部任务创建相关的请求和响应数据模型
"""
from typing import Optional, List, Dict, Any
from enum import Enum
from pydantic import BaseModel, Field
# 任务类型枚举
class TaskTypeEnum(str, Enum):
"""任务类型枚举"""
GG2MP = "GG2MP" # 高柜到MP
GGFK2MP = "GGFK2MP" # 高柜发库到MP
GT2MP = "GT2MP" # 高台到MP
GTFK2MP = "GTFK2MP" # 高台发库到MP
ZG2MP = "ZG2MP" # 中柜到MP
QZ2MP = "QZ2MP" # 清洗到MP
LG2MP = "LG2MP" # 料柜到MP
PHZ2MP = "PHZ2MP" # 配货站到MP
MP2GG = "MP2GG" # MP到高柜
MP2GGFK = "MP2GGFK" # MP到高柜发库
MP2GT = "MP2GT" # MP到高台
MP2GTFK = "MP2GTFK" # MP到高台发库
MP2ZG = "MP2ZG" # MP到中柜
MP2QZ = "MP2QZ" # MP到清洗
MP2LG = "MP2LG" # MP到料柜
MP2PHZ = "MP2PHZ" # MP到配货站
# 外部任务创建请求模型
class ExternalTaskRequest(BaseModel):
"""外部任务创建请求模型"""
ReqCode: str = Field(..., description="请求唯一标识码")
SourceID: str = Field("", description="来源ID")
TargetID: str = Field(..., description="目标ID")
TaskType: TaskTypeEnum = Field(..., description="任务类型")
# 外部任务创建响应模型
class ExternalTaskResponse(BaseModel):
"""外部任务创建响应模型"""
code: int = Field(..., description="响应码0表示成功")
reqCode: str = Field(..., description="请求唯一标识码")
message: str = Field(..., description="响应消息")
rowCount: int = Field(0, description="影响行数")
# 位置路径项模型
class PositionCodePath1(BaseModel):
"""位置路径项模型"""
PositionCode: str = Field(..., description="位置代码")
Type: str = Field(..., description="类型")
# AGV调度任务请求模型
class GenAgvSchedulingTaskRequest(BaseModel):
"""AGV调度任务请求模型"""
ReqCode: str = Field("", description="请求唯一标识码")
TaskTyp: str = Field("", description="任务类型")
SecurityKey: str = Field("", description="安全密钥")
Type: str = Field("", description="类型")
TaskCode: str = Field("", description="任务代码")
SubType: str = Field("", description="子类型")
AreaPositonCode: str = Field("", description="区域位置代码")
AreaPositonName: str = Field("", description="区域位置名称")
PositionCodePath: List[PositionCodePath1] = Field(..., description="位置代码路径")
ClientCode: str = Field("", description="客户端代码")
TokenCode: str = Field("", description="令牌代码")

View File

@ -118,6 +118,8 @@ class StorageLocationListRequest(BaseModel):
is_empty_tray: Optional[bool] = Field(None, description="是否空托盘") is_empty_tray: Optional[bool] = Field(None, description="是否空托盘")
include_operate_point_info: bool = Field(True, description="是否包含动作点信息") include_operate_point_info: bool = Field(True, description="是否包含动作点信息")
include_extended_fields: bool = Field(True, description="是否包含扩展字段") include_extended_fields: bool = Field(True, description="是否包含扩展字段")
layer_name_sort: Optional[bool] = Field(None, description="层名称排序true升序默认、false降序")
station_name_sort: Optional[bool] = Field(None, description="站点名称排序true升序默认、false降序")
page: int = Field(1, ge=1, description="页码") page: int = Field(1, ge=1, description="页码")
page_size: int = Field(20, ge=1, le=100, description="每页数量") page_size: int = Field(20, ge=1, le=100, description="每页数量")

View File

@ -78,13 +78,15 @@ async def get_storage_location_list(
scene_id: Optional[str] = Query(None, description="场景ID"), scene_id: Optional[str] = Query(None, description="场景ID"),
storage_area_id: Optional[str] = Query(None, description="库区ID"), storage_area_id: Optional[str] = Query(None, description="库区ID"),
station_name: Optional[str] = Query(None, description="站点名称(支持模糊搜索)"), station_name: Optional[str] = Query(None, description="站点名称(支持模糊搜索)"),
layer_name: Optional[str] = Query(None, description="名称(支持模糊搜索)"), layer_name: Optional[str] = Query(None, description="库位名称(支持模糊搜索)"),
is_disabled: Optional[bool] = Query(None, description="是否禁用"), is_disabled: Optional[bool] = Query(None, description="是否禁用"),
is_occupied: Optional[bool] = Query(None, description="是否占用"), is_occupied: Optional[bool] = Query(None, description="是否占用"),
is_locked: Optional[bool] = Query(None, description="是否锁定"), is_locked: Optional[bool] = Query(None, description="是否锁定"),
is_empty_tray: Optional[bool] = Query(None, description="是否空托盘"), is_empty_tray: Optional[bool] = Query(None, description="是否空托盘"),
include_operate_point_info: bool = Query(True, description="是否包含动作点信息"), include_operate_point_info: bool = Query(True, description="是否包含动作点信息"),
include_extended_fields: bool = Query(True, description="是否包含扩展字段"), include_extended_fields: bool = Query(True, description="是否包含扩展字段"),
layer_name_sort: Optional[bool] = Query(None, description="层名称排序true升序默认、false降序"),
station_name_sort: Optional[bool] = Query(None, description="站点名称排序true升序默认、false降序"),
page: int = Query(1, ge=1, description="页码"), page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"), page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: Session = Depends(get_db) db: Session = Depends(get_db)
@ -146,6 +148,8 @@ async def get_storage_location_list(
is_empty_tray=is_empty_tray, is_empty_tray=is_empty_tray,
include_operate_point_info=include_operate_point_info, include_operate_point_info=include_operate_point_info,
include_extended_fields=include_extended_fields, include_extended_fields=include_extended_fields,
layer_name_sort=layer_name_sort,
station_name_sort=station_name_sort,
page=page, page=page,
page_size=page_size page_size=page_size
) )

125
routes/storage_queue_api.py Normal file
View File

@ -0,0 +1,125 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
库位队列管理API
提供队列状态监控和管理接口
"""
from fastapi import APIRouter, HTTPException, Query
from typing import Dict, Any, Optional
from services.execution.handlers.storage_queue_manager import storage_queue_manager
from utils.api_response import success_response, error_response
from utils.logger import get_logger
logger = get_logger("routes.storage_queue_api")
router = APIRouter(prefix="/api/storage-queue", tags=["库位队列管理"])
@router.get("/stats", summary="获取队列统计信息")
async def get_queue_stats() -> Dict[str, Any]:
"""获取队列统计信息"""
try:
stats = storage_queue_manager.get_queue_stats()
return success_response(data=stats, message="获取队列统计信息成功")
except Exception as e:
logger.error(f"获取队列统计信息失败: {str(e)}")
return error_response(f"获取队列统计信息失败: {str(e)}")
@router.get("/status/{request_id}", summary="获取请求状态")
async def get_request_status(request_id: str) -> Dict[str, Any]:
"""获取特定请求的状态"""
try:
status = await storage_queue_manager.get_request_status(request_id)
if status:
return success_response(data=status, message="获取请求状态成功")
else:
return error_response("请求不存在")
except Exception as e:
logger.error(f"获取请求状态失败: {str(e)}")
return error_response(f"获取请求状态失败: {str(e)}")
@router.post("/cancel/{request_id}", summary="取消请求")
async def cancel_request(request_id: str) -> Dict[str, Any]:
"""取消指定的请求"""
try:
success = await storage_queue_manager.cancel_request(request_id)
if success:
return success_response(message=f"取消请求成功: {request_id}")
else:
return error_response("请求不存在或无法取消")
except Exception as e:
logger.error(f"取消请求失败: {str(e)}")
return error_response(f"取消请求失败: {str(e)}")
@router.get("/health", summary="队列健康检查")
async def queue_health_check() -> Dict[str, Any]:
"""检查队列管理器的健康状态"""
try:
stats = storage_queue_manager.get_queue_stats()
# 判断健康状态
health_status = "healthy"
issues = []
# 检查队列大小
if stats.get('queue_size', 0) > 800: # 队列接近满载
health_status = "warning"
issues.append("队列接近满载")
# 检查活跃工作者数量
if stats.get('active_workers', 0) == 0:
health_status = "critical"
issues.append("没有活跃的工作者")
# 检查失败率
total_requests = stats.get('requests_total', 0)
if total_requests > 0:
failure_rate = stats.get('requests_failed', 0) / total_requests
if failure_rate > 0.1: # 失败率超过10%
health_status = "warning"
issues.append(f"失败率过高: {failure_rate:.2%}")
return success_response(data={
"status": health_status,
"issues": issues,
"stats": stats
}, message="队列健康检查完成")
except Exception as e:
logger.error(f"队列健康检查失败: {str(e)}")
return error_response(f"队列健康检查失败: {str(e)}")
@router.post("/start", summary="启动队列管理器")
async def start_queue_manager() -> Dict[str, Any]:
"""启动队列管理器"""
try:
await storage_queue_manager.start()
return success_response(message="队列管理器启动成功")
except Exception as e:
logger.error(f"启动队列管理器失败: {str(e)}")
return error_response(f"启动队列管理器失败: {str(e)}")
@router.post("/stop", summary="停止队列管理器")
async def stop_queue_manager() -> Dict[str, Any]:
"""停止队列管理器"""
try:
await storage_queue_manager.stop()
return success_response(message="队列管理器停止成功")
except Exception as e:
logger.error(f"停止队列管理器失败: {str(e)}")
return error_response(f"停止队列管理器失败: {str(e)}")
@router.get("/config", summary="获取队列配置")
async def get_queue_config() -> Dict[str, Any]:
"""获取队列管理器配置"""
try:
config = {
"max_workers": storage_queue_manager.max_workers,
"max_queue_size": storage_queue_manager.max_queue_size,
"handlers_registered": list(storage_queue_manager.handlers.keys())
}
return success_response(data=config, message="获取队列配置成功")
except Exception as e:
logger.error(f"获取队列配置失败: {str(e)}")
return error_response(f"获取队列配置失败: {str(e)}")

View File

@ -14,7 +14,7 @@ from datetime import datetime, timedelta
from services.task_record_service import TaskRecordService from services.task_record_service import TaskRecordService
from services.operate_point_service import OperatePointService from services.operate_point_service import OperatePointService
from data.session import get_db from data.session import session_scope
from routes.model.operate_point_model import StorageLocationListRequest from routes.model.operate_point_model import StorageLocationListRequest
from utils.logger import get_logger from utils.logger import get_logger
@ -163,10 +163,15 @@ async def websocket_task_execution(
message = json.loads(data) message = json.loads(data)
await handle_websocket_message(websocket, task_record_id, message) await handle_websocket_message(websocket, task_record_id, message)
except json.JSONDecodeError: except json.JSONDecodeError:
await websocket.send_text(safe_json_dumps({ # 检查是否是字符串心跳
"type": "error", if data.strip() == "ping":
"message": "无效的JSON格式" await websocket.send_text("pong")
}, ensure_ascii=False)) logger.debug(f"收到字符串心跳ping已回复pong任务记录ID: {task_record_id}")
else:
await websocket.send_text(safe_json_dumps({
"type": "error",
"message": "无效的消息格式请发送JSON格式或字符串'ping'"
}, ensure_ascii=False))
except Exception as e: except Exception as e:
logger.error(f"处理WebSocket消息失败: {str(e)}") logger.error(f"处理WebSocket消息失败: {str(e)}")
await websocket.send_text(safe_json_dumps({ await websocket.send_text(safe_json_dumps({
@ -242,13 +247,20 @@ async def websocket_storage_location_status(
# 接收客户端消息 # 接收客户端消息
data = await websocket.receive_text() data = await websocket.receive_text()
try: try:
# 首先尝试解析为JSON
message = json.loads(data) message = json.loads(data)
await handle_storage_location_websocket_message(websocket, scene_id, message, filter_params) await handle_storage_location_websocket_message(websocket, scene_id, message, filter_params)
except json.JSONDecodeError: except json.JSONDecodeError:
await websocket.send_text(safe_json_dumps({ # 如果不是JSON格式检查是否是字符串心跳
"type": "error", if data.strip() == "ping":
"message": "无效的JSON格式" await websocket.send_text("pong")
}, ensure_ascii=False)) logger.debug(f"收到字符串心跳ping已回复pong场景ID: {scene_id}")
else:
await websocket.send_text(safe_json_dumps({
"type": "error",
"message": "无效的消息格式请发送JSON格式或字符串'ping'"
}, ensure_ascii=False))
logger.warning(f"收到无效消息格式: {data}")
except Exception as e: except Exception as e:
logger.error(f"处理库位状态WebSocket消息失败: {str(e)}") logger.error(f"处理库位状态WebSocket消息失败: {str(e)}")
await websocket.send_text(safe_json_dumps({ await websocket.send_text(safe_json_dumps({
@ -296,10 +308,11 @@ async def websocket_storage_location_broadcast(
try: try:
message = json.loads(data) message = json.loads(data)
if message.get("type") == "ping": if message.get("type") == "ping":
await websocket.send_text(safe_json_dumps({ await websocket.send_text("pong")
"type": "pong", except json.JSONDecodeError:
"timestamp": datetime.now().isoformat() # 支持字符串格式的心跳
}, ensure_ascii=False)) if data.strip() == "ping":
await websocket.send_text("pong")
except: except:
pass pass
except WebSocketDisconnect: except WebSocketDisconnect:
@ -325,10 +338,7 @@ async def handle_websocket_message(websocket: WebSocket, task_record_id: str, me
await send_task_execution_status(task_record_id, websocket) await send_task_execution_status(task_record_id, websocket)
elif message_type == "ping": elif message_type == "ping":
# 心跳检测 # 心跳检测
await websocket.send_text(safe_json_dumps({ await websocket.send_text("pong")
"type": "pong",
"timestamp": datetime.now().isoformat()
}, ensure_ascii=False))
else: else:
await websocket.send_text(safe_json_dumps({ await websocket.send_text(safe_json_dumps({
"type": "error", "type": "error",
@ -442,12 +452,12 @@ async def send_storage_location_status(scene_id: str, websocket: WebSocket, filt
""" """
try: try:
# 获取库位状态 # 获取库位状态
with get_db() as db: with session_scope() as db:
# 构建请求参数过滤掉None值 # 构建请求参数过滤掉None值
request_params = {k: v for k, v in filter_params.items() if v is not None} request_params = {k: v for k, v in filter_params.items() if v is not None}
# 设置默认分页参数 # 设置默认分页参数
request_params.setdefault("page", 1) request_params.setdefault("page", 1)
request_params.setdefault("page_size", 1000) # 默认获取大量数据 request_params.setdefault("page_size", 100) # 默认获取数据,符合验证规则
request = StorageLocationListRequest(**request_params) request = StorageLocationListRequest(**request_params)
result = OperatePointService.get_storage_location_list(db, request) result = OperatePointService.get_storage_location_list(db, request)
@ -503,11 +513,11 @@ async def periodic_push_storage_location_status(websocket: WebSocket, scene_id:
# 获取当前数据 # 获取当前数据
try: try:
with get_db() as db: with session_scope() as db:
# 构建请求参数过滤掉None值 # 构建请求参数过滤掉None值
request_params = {k: v for k, v in filter_params.items() if v is not None} request_params = {k: v for k, v in filter_params.items() if v is not None}
request_params.setdefault("page", 1) request_params.setdefault("page", 1)
request_params.setdefault("page_size", 1000) request_params.setdefault("page_size", 100)
request = StorageLocationListRequest(**request_params) request = StorageLocationListRequest(**request_params)
result = OperatePointService.get_storage_location_list(db, request) result = OperatePointService.get_storage_location_list(db, request)
@ -564,10 +574,7 @@ async def handle_storage_location_websocket_message(websocket: WebSocket, scene_
await send_storage_location_status(scene_id, websocket, filter_params) await send_storage_location_status(scene_id, websocket, filter_params)
elif message_type == "ping": elif message_type == "ping":
# 心跳检测 # 心跳检测
await websocket.send_text(safe_json_dumps({ await websocket.send_text("pong")
"type": "pong",
"timestamp": datetime.now().isoformat()
}, ensure_ascii=False))
else: else:
await websocket.send_text(safe_json_dumps({ await websocket.send_text(safe_json_dumps({
"type": "error", "type": "error",
@ -600,10 +607,11 @@ async def websocket_task_execution_broadcast(
try: try:
message = json.loads(data) message = json.loads(data)
if message.get("type") == "ping": if message.get("type") == "ping":
await websocket.send_text(safe_json_dumps({ await websocket.send_text("pong")
"type": "pong", except json.JSONDecodeError:
"timestamp": datetime.now().isoformat() # 支持字符串格式的心跳
}, ensure_ascii=False)) if data.strip() == "ping":
await websocket.send_text("pong")
except: except:
pass pass
except WebSocketDisconnect: except WebSocketDisconnect:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More