This commit is contained in:
靳中伟 2025-07-14 10:29:37 +08:00
parent b684132a73
commit 2899453300
151 changed files with 54094 additions and 544132 deletions

341
README.md
View File

@ -11,12 +11,14 @@ VWED任务模块采用分层架构设计遵循关注点分离原则各层
### 1. 架构层次
- **表现层API层**处理HTTP请求和响应提供RESTful API接口
- **业务逻辑层Service层**:实现核心业务逻辑,处理任务和工作流的创建、执行和管理
- **代理层Agents层**:提供智能代理服务,支持任务自动化和优化(已设计,尚未实现)
- **领域层Core层**:定义核心领域模型和业务规则,包含工作流引擎和组件系统
- **中间件层Middlewares层**:提供请求日志记录、错误处理、认证等横切关注点
- **业务逻辑层Services层**:实现核心业务逻辑,包含任务执行、智能调度、设备控制等服务
- **智能代理层Agents层**:提供智能代理服务,支持任务自动化和优化(已设计,部分实现)
- **领域层Core层**:定义核心领域模型和业务规则,包含智能服务等核心功能
- **数据层Data层**:提供数据模型定义和数据访问功能,支持任务、流程、组件等数据的持久化
- **智能服务层**:提供向量化和向量存储服务,支持知识检索和智能决策(已设计,尚未实现)
- **基础设施层**:提供数据持久化、日志记录、配置管理等基础服务
- **组件层Components层**:提供可扩展的组件系统,支持任务流程的模块化构建
- **工具层Utils层**:提供日志记录、数据验证、加密、数据库迁移等基础工具
- **配置层Config层**:提供系统配置管理,包含数据库、组件、错误信息等配置
### 2. 设计模式
@ -26,6 +28,7 @@ VWED任务模块采用分层架构设计遵循关注点分离原则各层
- **命令模式**:用于任务执行和撤销操作
- **组合模式**:用于构建组件树和工作流结构
- **仓储模式**:用于数据访问和持久化
- **中间件模式**:用于请求处理的横切关注点
### 3. 系统架构图
@ -38,7 +41,7 @@ VWED任务模块采用分层架构设计遵循关注点分离原则各层
+------------------+ +----------+---------+
| | | |
| 外部知识库 +------------->+ API层 |
| | | |
| | | (routes模块) |
+------------------+ +----+------+--------+
| |
| |
@ -47,100 +50,159 @@ VWED任务模块采用分层架构设计遵循关注点分离原则各层
v v
+----------+---------+ +---------+----------+
| | | |
| Agent系统 |<------------>| 业务服务层 |
| (包含LLM功能) | | |
| 中间件层 |<------------>| 业务服务层 |
| (请求日志/错误 | | (services模块) |
| 处理/认证) | | |
+----+---------------+ +--------+-----------+
| |
| |
| v
| +--------+-----------+
| | |
| | 工作流引擎 |
| | |
| | 任务执行引擎 |
| | (execution模块) |
| +--------+-----------+
| |
| |
| v
| +--------+-----------+
| | |
+----------------------------->+ 组件系统 |
| | |
+----------------------------->+ 智能代理系统 |
| | (agents模块) |
| +--------+-----------+
| |
v v
+----+---------------+ +--------+-----------+
| | | |
| 智能服务层 | | 外部系统/设备 |
| (向量化和向量存储) | | |
| (intelligence模块) | | (calldevice) |
+----+---------------+ +--------------------+
|
|
v
+----+---+
| |
|向量数据库|
| |
|数据存储层|
| (data) |
+--------+
```
> **注意**架构图中的Agent系统、外部知识库、智能服务层和向量数据库部分已完成设计但尚未实现。这些组件将在后续版本中逐步开发和集成
> **注意**智能代理系统和智能服务层已完成基础架构设计,正在逐步实现相关功能
## 项目结构
```
VWED_task/
├── routes/ # API路由定义
│ ├── task_api.py # 任务相关API
│ ├── task_edit_api.py # 任务编辑API
│ ├── script_api.py # 脚本相关API
│ ├── template_api.py # 模板相关API
│ ├── common_api.py # 通用API
│ ├── database.py # 数据库API
│ └── model/ # API模型定义
├── services/ # 业务服务层
│ ├── task_service.py # 任务服务
├── agents/ # 智能代理层(已设计,尚未实现)
│ ├── base_agent.py # 代理基类
│ ├── llm/ # 大语言模型集成
│ ├── prompts/ # 提示词模板
│ ├── tools/ # 代理工具集
│ └── teams/ # 代理团队协作
├── components/ # 组件实现
│ ├── base_components.py # 基础组件
├── core/ # 核心功能模块
│ ├── component.py # 组件基类定义
│ ├── context.py # 执行上下文
│ ├── exceptions.py # 异常定义
│ └── workflow.py # 工作流引擎
├── data/ # 数据存储
│ ├── models/ # 数据模型定义
│ │ ├── base.py # 基础模型
│ ├── session.py # 数据库会话管理
│ └── repositories/ # 数据仓储实现
├── migrations/ # 数据库迁移
│ ├── versions/ # 迁移版本
│ ├── env.py # 迁移环境配置
│ ├── script.py.mako # 迁移脚本模板
│ └── alembic.ini # Alembic配置
├── scripts/ # 脚本工具
│ ├── generate_migration.py # 生成迁移脚本
│ └── run_migration.py # 执行迁移脚本
├── utils/ # 工具函数
│ ├── logger.py # 日志工具
│ ├── validators.py # 数据验证工具
│ └── helpers.py # 辅助函数
├── config/ # 配置文件
│ ├── database.py # 数据库配置
├── logs/ # 日志文件
├── docs/ # 文档
├── tests/ # 测试代码
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── fixtures/ # 测试数据
├── app.py # 应用入口
├── Dockerfile # Docker配置文件
├── docker-compose.yml # Docker Compose配置
└── requirements.txt # 依赖包列表
├── routes/ # API路由定义
│ ├── task_api.py # 任务相关API
│ ├── task_edit_api.py # 任务编辑API
│ ├── task_record_api.py # 任务记录API
│ ├── script_api.py # 脚本相关API
│ ├── template_api.py # 模板相关API
│ ├── calldevice_api.py # 设备调用API
│ ├── modbus_config_api.py # Modbus配置API
│ ├── common_api.py # 通用API
│ ├── database.py # 数据库API
│ ├── __init__.py # 路由模块初始化
│ └── model/ # API模型定义
├── services/ # 业务服务层
│ ├── task_service.py # 任务服务
│ ├── task_edit_service.py # 任务编辑服务
│ ├── task_record_service.py # 任务记录服务
│ ├── script_service.py # 脚本服务
│ ├── template_service.py # 模板服务
│ ├── calldevice_service.py # 设备调用服务
│ ├── modbus_config_service.py# Modbus配置服务
│ ├── sync_service.py # 同步服务
│ ├── execution/ # 任务执行模块
│ │ ├── task_executor.py # 任务执行器
│ │ ├── block_executor.py # 任务块执行器
│ │ ├── task_context.py # 执行上下文
│ │ ├── block_handlers.py # 任务块处理器
│ │ └── handlers/ # 处理器集合
│ ├── enhanced_scheduler/ # 增强版任务调度器
│ ├── intelligence/ # 智能服务模块
│ │ └── embedding.py # 嵌入模型服务
│ ├── agent/ # 代理服务模块
│ └── __init__.py # 服务模块初始化
├── middlewares/ # 中间件层
│ ├── request_logger.py # 请求日志中间件
│ ├── error_handlers.py # 错误处理中间件
│ └── __init__.py # 中间件初始化
├── agents/ # 智能代理层
│ ├── base_agent.py # 代理基类(规划中)
│ ├── llm/ # 大语言模型集成
│ ├── prompts/ # 提示词模板
│ ├── tools/ # 代理工具集
│ ├── teams/ # 代理团队协作
│ └── __init__.py # 代理模块初始化
├── components/ # 组件实现
│ ├── base_components.py # 基础组件
│ └── __init__.py # 组件模块初始化
├── core/ # 核心功能模块
│ ├── intelligence/ # 智能服务核心
│ └── __init__.py # 核心模块初始化
├── data/ # 数据存储
│ ├── models/ # 数据模型定义
│ │ ├── base.py # 基础模型
│ │ ├── taskdef.py # 任务定义模型
│ │ ├── taskrecord.py # 任务记录模型
│ │ ├── blockrecord.py # 任务块记录模型
│ │ ├── script.py # 脚本模型
│ │ ├── tasktemplate.py # 任务模板模型
│ │ ├── calldevice.py # 设备调用模型
│ │ ├── modbusconfig.py # Modbus配置模型
│ │ ├── interfacedef.py # 接口定义模型
│ │ ├── tasklog.py # 任务日志模型
│ │ ├── datacache.py # 数据缓存模型
│ │ ├── datacachesplit.py # 数据缓存分片模型
│ │ └── __init__.py # 模型模块初始化
│ ├── enum/ # 枚举定义
│ ├── task_backups/ # 任务备份存储
│ ├── session.py # 数据库会话管理
│ ├── cache.py # 缓存服务
│ └── __init__.py # 数据模块初始化
├── migrations/ # 数据库迁移
│ ├── versions/ # 迁移版本
│ ├── env.py # 迁移环境配置
│ ├── script.py.mako # 迁移脚本模板
│ └── alembic.ini # Alembic配置
├── scripts/ # 脚本工具
│ ├── generate_migration.py # 生成迁移脚本
│ └── run_migration.py # 执行迁移脚本
├── utils/ # 工具函数
│ ├── logger.py # 日志工具
│ ├── validator.py # 数据验证工具
│ ├── helpers.py # 辅助函数
│ ├── api_response.py # API响应工具
│ ├── component_manager.py # 组件管理器
│ ├── built_in_functions.py # 内置函数
│ ├── crypto_utils.py # 加密工具
│ ├── db_migration.py # 数据库迁移工具
│ ├── string_utils/ # 字符串处理工具
│ └── __init__.py # 工具模块初始化
├── config/ # 配置文件
│ ├── settings.py # 系统设置
│ ├── database_config.py # 数据库配置
│ ├── tf_api_config.py # TF API配置
│ ├── error_messages.py # 错误信息配置
│ ├── components/ # 组件配置
│ └── __init__.py # 配置模块初始化
├── exports/ # 导出文件存储
├── logs/ # 日志文件
├── docs/ # 文档
├── tests/ # 测试代码
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── fixtures/ # 测试数据
├── VWED任务模块接口文档/ # API接口文档
├── app.py # 应用入口
├── Dockerfile # Docker配置文件
├── docker-compose.yml # Docker Compose配置
├── requirements.txt # 依赖包列表
├── VWED任务系统模块数据详情表.md # 数据表详情文档
├── VWED任务模块系统架构图文件.md # 系统架构详细文档
└── README.md # 项目说明文档
```
## 核心功能
@ -165,55 +227,53 @@ VWED任务模块提供了强大的可视化编辑器支持以下功能
- **源代码生成**:自动生成任务执行的源代码
- **版本备份**:支持任务配置的版本备份和恢复
### 3. 组件库
### 3. 任务执行引擎
VWED任务模块提供了丰富的组件库用户可以通过拖拽组件和配置参数的方式设计复杂的任务流程
系统提供了完整的任务执行引擎,包含以下组件
- **子任务组件**:支持任务的模块化和复用
- **脚本组件**支持编写自定义JavaScript脚本
- **HTTP请求组件**:支持与外部系统进行通信
- **任务组件**:提供任务数据管理和状态控制功能
- **流程组件**:提供条件判断、循环、并行执行等流程控制功能
- **基础组件**提供数据验证、ID生成、时间处理等基础功能
- **库位组件**:提供库位管理和操作功能
- **机器人调度组件**:提供机器人选择、控制和状态监控功能
- **设备组件**:提供与外部设备通信的功能
- **任务执行器TaskExecutor**:负责任务的整体执行控制
- **任务块执行器BlockExecutor**:处理任务中各个执行块的执行逻辑
- **执行上下文TaskContext**:管理任务执行过程中的变量和状态
- **任务块处理器BlockHandlers**:提供各种类型任务块的具体执行逻辑
- **增强版调度器EnhancedScheduler**:提供高性能的任务调度服务
### 4. 智能代理系统(已设计,尚未实现)
### 4. 设备集成与控制
- **任务代理**:自动分析和优化任务流程
- **设备调用服务**:统一的设备调用接口,支持与各种外部设备通信
- **Modbus配置管理**支持Modbus协议设备的配置和通信
- **接口定义管理**:灵活的接口定义系统,支持自定义设备接口
### 5. 智能代理系统(部分实现)
- **代理基础架构**:提供代理系统的基础框架
- **LLM集成**:集成大语言模型,提供智能决策支持
- **代理工具**:提供丰富的工具集,支持代理执行各种操作
- **团队协作**:支持多代理协作完成复杂任务
- **知识检索**:从外部知识库中检索相关信息,辅助决策
### 5. 智能服务层(已设计,尚未实现)
### 6. 智能服务层(部分实现)
- **向量化服务**:将文本、图像等数据转换为向量表示
- **知识检索**:基于语义相似度进行知识检索
- **嵌入模型**:提供多种嵌入模型,支持不同类型数据的向量化
- **向量存储**:高效存储和检索向量数据
- **嵌入模型服务**:提供文本向量化和语义分析能力
- **知识检索**:基于语义相似度进行知识检索(规划中)
- **智能决策支持**基于AI模型的智能决策辅助规划中
### 6. 工作流引擎
### 7. 中间件系统
- **流程执行**:解析和执行任务流程图
- **上下文管理**:管理任务执行过程中的变量和状态
- **错误处理**:提供异常捕获和处理机制
- **并行执行**:支持多分支并行执行
- **动态加载**:支持动态加载和执行组件
- **请求日志中间件**自动记录API请求日志便于调试和监控
- **错误处理中间件**:统一的错误处理机制,提供友好的错误信息
- **认证中间件**:用户认证和权限控制(规划中)
### 7. 数据模型
### 8. 数据模型
系统提供了完善的数据模型,支持任务管理的各个方面:
- **任务模型**:存储任务基本信息和配置
- **任务版本**:管理任务的不同版本
- **任务记录**:记录任务的执行情况
- **任务流程节点**:存储流程图中的节点信息
- **任务流程连接**:存储流程图中的连接信息
- **任务变量定义**:管理任务中使用的变量
- **任务编辑历史**:记录编辑操作,支持撤销/重做
- **任务备份**:存储任务的备份数据
- **任务定义模型TaskDef**:存储任务基本信息和配置
- **任务记录模型TaskRecord**:记录任务的执行情况
- **任务块记录模型BlockRecord**:存储任务执行过程中各个块的记录
- **脚本模型Script**:管理自定义脚本
- **任务模板模型TaskTemplate**:支持任务模板功能
- **设备调用模型CallDevice**:管理设备调用配置
- **Modbus配置模型ModbusConfig**管理Modbus设备配置
- **数据缓存模型DataCache**:提供数据缓存功能
## 执行业务流程
@ -223,9 +283,10 @@ VWED任务模块提供了丰富的组件库用户可以通过拖拽组件和
2. 加载配置信息config模块
3. 初始化日志系统utils.logger
4. 初始化数据库连接data.session
5. 注册所有组件config.component_registry
6. 注册API路由api模块
7. 启动HTTP服务uvicorn
5. 初始化中间件middlewares模块
6. 注册API路由routes模块
7. 启动增强版任务调度器services.enhanced_scheduler
8. 启动HTTP服务uvicorn
### 任务创建流程
@ -236,45 +297,30 @@ VWED任务模块提供了丰富的组件库用户可以通过拖拽组件和
5. 前端跳转到任务编辑页面
6. 用户通过拖拽组件设计任务流程
7. 前端发送保存流程请求到API层/api/workflows
8. API层调用工作流服务workflow_service.py保存流程配置
8. API层调用任务编辑服务task_edit_service.py保存流程配置
9. 系统将流程配置保存到数据库,创建新版本
### 任务编辑流程
1. 用户打开任务编辑页面
2. 系统加载任务的最新版本和流程图数据
3. 用户通过拖拽组件和连接设计流程图
4. 用户配置组件属性和变量
5. 系统记录每一步编辑操作,支持撤销/重做
6. 用户点击保存按钮,系统创建新版本
7. 用户可以点击测试按钮,在测试环境中执行任务
8. 用户可以点击生成源码按钮,系统生成任务执行代码
### 任务执行流程
1. 用户在任务列表中选择任务并点击"执行"按钮
2. 前端发送执行任务请求到API层/api/tasks/{id}/execute
3. API层调用任务服务task_service.py创建任务记录
4. 任务服务加载任务配置和工作流定义
5. 任务服务初始化执行上下文context.py
6. 任务服务调用工作流引擎workflow.py执行任务
7. 工作流引擎解析任务流程图并按顺序执行各组件
8. 组件执行结果存储在上下文中,供后续组件使用
4. 任务服务将任务提交到增强版调度器
5. 调度器分配工作线程,初始化任务执行器
6. 任务执行器加载任务配置,创建执行上下文
7. 任务执行器调用任务块执行器执行各个任务块
8. 任务块执行器根据任务块类型调用相应的处理器
9. 系统实时更新任务状态和进度
10. 任务执行完成后,系统记录执行结果和日志
### 智能任务执行流程(已设计,尚未实现)
### 设备控制流程
1. 用户在任务列表中选择任务并点击"智能执行"按钮
2. 前端发送智能执行任务请求到API层
3. API层调用Agent系统进行任务分析和优化
4. Agent系统从外部知识库检索相关知识
5. Agent系统基于LLM和检索到的知识生成执行计划
6. Agent系统调用业务服务层执行优化后的任务
7. 业务服务层调用工作流引擎执行任务
8. 执行过程中Agent系统持续监控任务状态
9. 遇到异常情况时Agent系统自动调整执行策略
10. 任务执行完成后Agent系统生成执行报告和优化建议
1. 任务执行过程中遇到设备控制任务块
2. 任务块执行器调用设备调用服务
3. 设备调用服务根据配置选择相应的通信协议
4. 通过Modbus、HTTP等协议与设备通信
5. 接收设备响应并更新任务执行状态
6. 将设备操作结果记录到任务日志
## 交互方式
@ -302,16 +348,16 @@ VWED任务模块提供多种交互方式满足不同场景的需求
## 技术栈
- **后端**FastAPI (Python)
- **后端**FastAPI (Python 3.11+)
- **数据库**SQLAlchemy ORM支持MySQL、PostgreSQL等
- **前端**React + Ant Design低代码编辑器
- **API文档**Swagger UI自动生成
- **工作流引擎**自研基于DAG的工作流引擎
- **任务调度**:自研增强版异步任务调度器
- **组件系统**:可扩展的组件注册和执行系统
- **数据迁移**Alembic
- **智能代理**:基于大语言模型的智能代理系统(已设计,尚未实现)
- **向量数据库**:支持高效的向量存储和检索(已设计,尚未实现)
- **嵌入模型**:支持文本、图像等数据的向量化(已设计,尚未实现)
- **智能代理**:基于大语言模型的智能代理系统(部分实现)
- **设备通信**支持Modbus、HTTP等多种协议
- **缓存系统**:基于内存和数据库的多级缓存
## 部署说明
@ -400,7 +446,7 @@ conda activate pytf
pip install -r requirements.txt
# 4. 配置数据库连接
# 编辑 config/database.py 文件,设置正确的数据库连接信息
# 编辑 config/database_config.py 文件,设置正确的数据库连接信息
# 5. 启动应用
python app.py
@ -408,7 +454,7 @@ python app.py
### 数据库配置说明
在运行应用前,需要修改`config/database.py`文件中的数据库配置将默认的数据库连接信息替换为您自己的本地或云上的MySQL数据库
在运行应用前,需要修改`config/database_config.py`文件中的数据库配置将默认的数据库连接信息替换为您自己的本地或云上的MySQL数据库
```python
# 数据库连接配置
@ -472,7 +518,7 @@ conda activate pytf
pip install -r requirements.txt
# 4. 配置数据库连接
# 编辑 config/database.py 文件,设置正确的数据库连接信息
# 编辑 config/database_config.py 文件,设置正确的数据库连接信息
# 5. 启动应用
python app.py
@ -493,7 +539,7 @@ conda activate pytf
pip install -r requirements.txt
# 4. 配置数据库连接
# 编辑 config/database.py 文件,设置正确的数据库连接信息
# 编辑 config/database_config.py 文件,设置正确的数据库连接信息
# 5. 启动应用
python app.py
@ -518,7 +564,7 @@ python -m venv venv
pip install -r requirements.txt
# 4. 配置数据库连接
# 编辑 config/database.py 文件
# 编辑 config/database_config.py 文件
# 5. 启动应用
python app.py
@ -539,7 +585,7 @@ source venv/bin/activate
pip install -r requirements.txt
# 4. 配置数据库连接
# 编辑 config/database.py 文件
# 编辑 config/database_config.py 文件
# 5. 启动应用
python app.py
@ -549,7 +595,7 @@ python app.py
1. 克隆代码库
2. 安装依赖:`pip install -r requirements.txt`
3. 配置数据库连接:修改`config/database.py`
3. 配置数据库连接:修改`config/database_config.py`
4. 启动应用:`python app.py`
## 使用示例
@ -597,6 +643,11 @@ python app.py
- 使用子任务功能将复杂任务拆分为多个子任务
- 使用标签和分类功能组织任务
4. **设备通信问题怎么解决?**
- 检查设备配置参数是否正确
- 验证网络连接和防火墙设置
- 查看设备通信日志,定位通信故障
## 联系方式
如有问题或建议,请联系系统管理员或开发团队。

View File

@ -0,0 +1,300 @@
# Modbus配置接口文档
本文档描述了VWED系统中Modbus配置模块的API接口包括Modbus设备配置的添加、删除、修改、查询等功能。
## 1. 新增Modbus配置
### 接口描述
添加新的Modbus设备配置信息。
### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/api/vwed-modbus-config/add`
### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| name | String | 是 | 配置名称 |
| ip | String | 是 | 设备IP地址 |
| port | Integer | 是 | 通信端口号 |
| slave_id | Integer | 是 | 从站ID |
| address_type | String | 是 | 地址类型("0X"-线圈寄存器, "1X"-离散输入寄存器, "3X"-输入寄存器, "4X"-保持寄存器) |
| address_number | Integer | 是 | 地址编号 |
| task_id | String | 否 | 关联的任务ID |
| target_value | Integer | 否 | 目标值 |
| reset_after_trigger | Boolean | 否 | 触发后是否重置默认为false |
| reset_signal_address | Integer | 否 | 重置信号地址 |
| reset_value | Integer | 否 | 重置值 |
| remark | String | 否 | 备注信息 |
| tenant_id | String | 否 | 租户ID |
### 请求示例
```json
{
"name": "测试Modbus配置",
"ip": "192.168.1.100",
"port": 502,
"slave_id": 1,
"address_type": "4X",
"address_number": 1000,
"target_value": 1,
"remark": "设备1号控制器"
}
```
### 响应参数
```json
{
"code": 200,
"message": "Modbus配置添加成功",
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "测试Modbus配置"
}
}
```
## 2. 删除Modbus配置
### 接口描述
删除指定的Modbus配置信息软删除
### 请求方式
- **HTTP方法**: DELETE
- **接口路径**: `/api/vwed-modbus-config/{config_id}`
### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| config_id | String | 是 | 配置ID (URL路径参数) |
### 响应参数
```json
{
"code": 200,
"message": "Modbus配置删除成功",
"data": null
}
```
## 3. 修改Modbus配置
### 接口描述
修改已有的Modbus配置信息。
### 请求方式
- **HTTP方法**: PUT
- **接口路径**: `/api/vwed-modbus-config/{config_id}`
### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| config_id | String | 是 | 配置ID (URL路径参数) |
| name | String | 否 | 配置名称 |
| ip | String | 否 | 设备IP地址 |
| port | Integer | 否 | 通信端口号 |
| slave_id | Integer | 否 | 从站ID |
| address_type | String | 否 | 地址类型 |
| address_number | Integer | 否 | 地址编号 |
| task_id | String | 否 | 关联的任务ID |
| target_value | Integer | 否 | 目标值 |
| remark | String | 否 | 备注信息 |
### 请求示例
```json
{
"name": "更新后的配置名称",
"ip": "192.168.1.101",
"port": 503,
"remark": "已更新的备注信息"
}
```
### 响应参数
```json
{
"code": 200,
"message": "Modbus配置更新成功",
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}
```
## 4. 获取Modbus配置详情
### 接口描述
获取指定Modbus配置的详细信息。
### 请求方式
- **HTTP方法**: GET
- **接口路径**: `/api/vwed-modbus-config/{config_id}`
### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| config_id | String | 是 | 配置ID (URL路径参数) |
### 响应参数
```json
{
"code": 200,
"message": "获取Modbus配置详情成功",
"data": {
"name": "测试Modbus配置",
"ip": "192.168.1.100",
"port": 502,
"slave_id": 1,
"address_type": "4X",
"address_number": 1000,
"task_id": "b1b2c3d4-e5f6-7890-abcd-ef1234567890",
"target_value": 1,
"remark": "设备1号控制器"
}
}
```
## 5. 获取Modbus配置列表
### 接口描述
获取系统中所有Modbus配置的列表信息支持分页查询和条件筛选。
### 请求方式
- **HTTP方法**: GET
- **接口路径**: `/api/vwed-modbus-config/list`
### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| page | Integer | 否 | 页码默认为1 |
| size | Integer | 否 | 每页记录数默认为10 |
| name | String | 否 | 配置名称,用于筛选 |
| ip | String | 否 | 设备IP地址用于筛选 |
### 响应参数
```json
{
"code": 200,
"message": "获取Modbus配置列表成功",
"data": {
"records": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "测试Modbus配置1",
"ip": "192.168.1.100",
"port": 502,
"slave_id": 1,
"address_type": "4X",
"address_number": 1000,
"task_id": "b1b2c3d4-e5f6-7890-abcd-ef1234567890",
"task_name":"20",
"target_value": 1,
"remark": "设备1号控制器",
"status": 1,
"create_date": "2023-05-10 14:30:22"
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f123456789ab",
"name": "测试Modbus配置2",
"ip": "192.168.1.101",
"port": 503,
"slave_id": 2,
"address_type": "3X",
"address_number": 2000,
"task_id": "c1b2c3d4-e5f6-7890-abcd-ef1234567890",
"task_name":"20",
"target_value": 0,
"remark": "设备2号控制器",
"status": 1,
"create_date": "2023-05-11 10:45:33"
}
],
"total": 2,
"size": 10,
"current": 1
}
}
```
## 6. 测试Modbus连接
### 接口描述
测试与Modbus设备的连接是否正常。
### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/api/vwed-modbus-config/test-connection`
### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| ip | String | 是 | 设备IP地址 |
| port | Integer | 是 | 通信端口号 |
| slave_id | Integer | 是 | 从站ID |
| address_type | String | 否 | 地址类型 |
| address_number | Integer | 否 | 地址编号 |
### 请求示例
```json
{
"ip": "192.168.1.100",
"port": 502,
"slave_id": 1,
"address_type": "4X",
"address_number": 1000
}
```
### 响应参数
```json
{
"code": 200,
"message": "Modbus连接测试成功",
"data": {
"connected": true,
"current_value": 12345
}
}
```
## 数据模型说明
### Modbus地址类型
| 类型值 | 说明 |
|-------|------|
| 0X | 线圈寄存器 |
| 1X | 离散输入寄存器 |
| 3X | 输入寄存器 |
| 4X | 保持寄存器 |
### 状态码说明
| 状态码 | 说明 |
|-------|------|
| 200 | 操作成功 |
| 400 | 请求参数错误 |
| 404 | 未找到指定资源 |
| 500 | 服务器内部错误 |
### 配置状态说明
| 状态值 | 说明 |
|-------|------|
| 1 | 启用 |
| 0 | 禁用 |

View File

@ -0,0 +1,301 @@
# VWED WebSocket接口文档
本文档描述了VWED系统WebSocket相关的API接口主要用于实时推送任务执行结果和状态更新。
## 基础信息
- 基础路径:`/ws`
- 接口标签:`WebSocket`
- 协议WebSocket协议ws://或wss://
## 接口清单
| 序号 | 接口名称 | 接口路径 | 协议 | 接口描述 |
| --- | --- | --- | --- | --- |
| 1 | 任务执行结果实时推送 | `/task-execution/{task_record_id}` | WebSocket | 实时推送指定任务记录的执行结果更新 |
| 2 | 任务执行结果广播 | `/task-execution-broadcast/{task_record_id}` | WebSocket | 接收任务执行结果广播消息 |
## 接口详情
### 1. 任务执行结果实时推送
#### 接口说明
建立WebSocket连接实时接收指定任务记录的执行结果更新。服务器会定期推送任务状态变化客户端也可以主动请求获取当前状态。
#### 连接路径
```
ws://your-domain/ws/task-execution/{task_record_id}?interval={interval}
```
#### 路径参数
| 参数名 | 类型 | 是否必须 | 描述 |
| --- | --- | --- | --- |
| task_record_id | string | 是 | 任务记录ID |
#### 查询参数
| 参数名 | 类型 | 是否必须 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| interval | integer | 否 | 2 | 推送间隔范围1-30秒 |
#### 客户端消息格式
客户端可以向服务器发送以下格式的JSON消息
##### 心跳检测
```json
{
"type": "ping",
"timestamp": "2025-06-11T12:00:00.000Z"
}
```
##### 获取当前状态
```json
{
"type": "get_status",
"timestamp": "2025-06-11T12:00:00.000Z"
}
```
#### 服务器消息格式
##### 任务执行结果更新
```json
{
"type": "task_execution_update",
"task_record_id": "任务记录ID",
"timestamp": "2025-06-11T12:00:00.000Z",
"message": "成功获取任务记录执行结果",
"data": [
{
"created_at": "2025-06-11T12:00:00.000Z",
"context": "[块执行名称] 执行内容描述",
"status": "SUCCESS/FAILED/RUNNING"
}
]
}
```
##### 心跳响应
```json
{
"type": "pong",
"timestamp": "2025-06-11T12:00:00.000Z"
}
```
##### 错误消息
```json
{
"type": "error",
"task_record_id": "任务记录ID",
"timestamp": "2025-06-11T12:00:00.000Z",
"message": "错误描述信息"
}
```
#### 响应字段说明
##### 任务执行结果字段
| 字段名 | 类型 | 描述 |
| --- | --- | --- |
| type | string | 消息类型,固定为"task_execution_update" |
| task_record_id | string | 任务记录ID |
| timestamp | string | 消息时间戳ISO 8601格式 |
| message | string | 响应消息描述 |
| data | array | 执行结果数组 |
| data[].created_at | string | 结果创建时间ISO 8601格式 |
| data[].context | string | 执行内容描述 |
| data[].status | string | 执行状态SUCCESS成功、FAILED失败、RUNNING执行中 |
#### 连接示例
##### JavaScript客户端示例
```javascript
// 建立WebSocket连接
const taskRecordId = "your-task-record-id";
const interval = 2; // 推送间隔2秒
const wsUrl = `ws://localhost:8000/ws/task-execution/${taskRecordId}?interval=${interval}`;
const websocket = new WebSocket(wsUrl);
// 连接建立
websocket.onopen = function(event) {
console.log("WebSocket连接已建立");
// 发送心跳包
websocket.send(JSON.stringify({
type: "ping",
timestamp: new Date().toISOString()
}));
};
// 接收消息
websocket.onmessage = function(event) {
const data = JSON.parse(event.data);
switch(data.type) {
case "task_execution_update":
console.log("任务执行结果更新:", data.data);
break;
case "pong":
console.log("心跳响应:", data.timestamp);
break;
case "error":
console.error("服务器错误:", data.message);
break;
}
};
// 连接关闭
websocket.onclose = function(event) {
console.log("WebSocket连接已关闭");
};
// 连接错误
websocket.onerror = function(error) {
console.error("WebSocket连接错误:", error);
};
```
##### Python客户端示例
```python
import asyncio
import json
import websockets
async def websocket_client():
task_record_id = "your-task-record-id"
interval = 2
uri = f"ws://localhost:8000/ws/task-execution/{task_record_id}?interval={interval}"
async with websockets.connect(uri) as websocket:
print("WebSocket连接已建立")
# 发送心跳包
await websocket.send(json.dumps({
"type": "ping",
"timestamp": datetime.now().isoformat()
}))
# 监听消息
async for message in websocket:
data = json.loads(message)
if data["type"] == "task_execution_update":
print(f"任务执行结果更新: {data['data']}")
elif data["type"] == "pong":
print(f"心跳响应: {data['timestamp']}")
elif data["type"] == "error":
print(f"服务器错误: {data['message']}")
# 运行客户端
asyncio.run(websocket_client())
```
#### 特性说明
1. **智能推送**:服务器只在数据发生变化时才推送更新,避免不必要的网络流量
2. **心跳检测**:支持客户端主动发送心跳包,维持连接活跃状态
3. **错误处理**:完善的错误处理机制,连接异常时自动清理资源
4. **状态查询**:客户端可随时主动请求获取当前任务状态
5. **多客户端支持**:同一任务记录可支持多个客户端同时连接
### 2. 任务执行结果广播
#### 接口说明
建立WebSocket连接接收任务执行结果的广播消息。与实时推送接口的区别在于此接口主要用于被动接收广播不会主动定期推送。
#### 连接路径
```
ws://your-domain/ws/task-execution-broadcast/{task_record_id}
```
#### 路径参数
| 参数名 | 类型 | 是否必须 | 描述 |
| --- | --- | --- | --- |
| task_record_id | string | 是 | 任务记录ID |
#### 客户端消息格式
##### 心跳检测
```json
{
"type": "ping",
"timestamp": "2025-06-11T12:00:00.000Z"
}
```
#### 服务器消息格式
与任务执行结果实时推送接口相同,参见上述文档。
#### 使用场景
1. **监控面板**:多个监控客户端同时监听任务状态变化
2. **日志收集**:收集任务执行过程中的状态变化记录
3. **事件通知**:当任务状态发生变化时接收通知
## 错误码说明
| 错误码 | 描述 | 解决方案 |
| --- | --- | --- |
| 1006 | 连接异常关闭 | 检查网络连接,重新建立连接 |
| 1011 | 服务器内部错误 | 检查服务器状态和日志 |
| 1013 | 临时服务不可用 | 稍后重试连接 |
## 最佳实践
### 1. 连接管理
- 实现连接断开后的自动重连机制
- 合理设置推送间隔,避免过于频繁的请求
- 及时关闭不需要的连接,释放服务器资源
### 2. 错误处理
- 监听`onerror``onclose`事件,处理连接异常
- 实现重连退避策略,避免连接风暴
- 记录错误日志,便于问题排查
### 3. 性能优化
- 使用合适的推送间隔建议2-5秒
- 客户端及时处理接收到的消息,避免消息积压
- 对于不活跃的任务,考虑降低推送频率
### 4. 安全考虑
- 在生产环境中使用WSS协议WebSocket Secure
- 实现适当的身份验证和授权机制
- 限制连接数量,防止资源滥用
## 注意事项
1. **任务记录ID有效性**确保传入的任务记录ID存在且有效
2. **网络稳定性**WebSocket连接对网络质量要求较高不稳定的网络可能导致频繁断连
3. **浏览器兼容性**确保目标浏览器支持WebSocket协议
4. **资源清理**页面关闭或组件销毁时及时关闭WebSocket连接
5. **消息处理**合理处理接收到的消息避免阻塞UI线程
## 更新日志
| 版本 | 日期 | 更新内容 |
| --- | --- | --- |
| 1.0.0 | 2025-06-11 | 初始版本,支持任务执行结果实时推送和广播功能 |

View File

@ -16,6 +16,7 @@
| 3 | 终止任务 | `/stop/{task_record_id}` | POST | 停止指定任务记录下的所有运行任务实例 |
| 4 | 获取任务执行结果 | `/execution/block/results/{task_record_id}` | GET | 获取指定任务记录的执行结果 |
| 5 | 获取任务记录详情 | `/detail/{task_record_id}` | GET | 获取指定任务记录的详细信息 |
| 15 | 设置任务错误状态 | `/set-error/{task_record_id}` | POST | 将指定任务记录及其相关任务块状态设置为错误状态 |
## 接口详情
@ -695,4 +696,71 @@ GET /api/vwed-task-record/detail/7b84fd4d-e947-4ec7-8535-05ede5f8aa9d
"source_remarks": "测试任务"
}
}
```
```
### 15. 设置任务错误状态
#### 接口说明
将指定任务记录及其相关任务块状态设置为错误状态
#### 请求信息
- **URL**: `/api/vwed-task-record/set-error/{task_record_id}`
- **Method**: `POST`
- **Content-Type**: `application/json`
#### 路径参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| task_record_id | string | 是 | 任务记录ID |
#### 请求体参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|--------|------|------|------|------|
| error_reason | string | 是 | 错误原因 | "系统异常导致任务失败" |
#### 请求示例
```json
{
"error_reason": "系统异常导致任务失败"
}
```
#### 响应示例
```json
{
"success": true,
"message": "任务状态设置为错误成功",
"data": {
"task_record_id": "123456",
"task_status": 2000,
"error_reason": "系统异常导致任务失败",
"ended_on": "2024-01-01T12:00:00",
"updated_blocks_count": 5,
"updated_blocks": [
{
"block_id": "block_001",
"block_name": "数据处理",
"status": 2000
}
]
}
}
```
#### 状态码说明
- **200**: 成功
- **400**: 请求参数错误
- **404**: 任务记录不存在
- **500**: 服务器内部错误
#### 功能说明
1. 如果任务正在运行,会先尝试取消任务
2. 将任务状态设置为失败状态2000
3. 将所有未完成的任务块状态设置为失败状态2000
4. 记录错误原因和结束时间
5. 返回更新的任务块数量和详细信息
#### 注意事项
- 只有未完成的任务块(非成功或失败状态)才会被更新
- 已经完成或失败的任务块不会被重复更新
- 如果任务记录不存在会返回404错误

View File

@ -0,0 +1,880 @@
# VWED呼叫器设备接口文档
## 接口概述
本文档描述了VWED系统中呼叫器设备管理相关的API接口。这些接口用于管理系统中的呼叫器设备包括新增、修改、删除、查询等操作。
## 接口列表
### 1. 新增呼叫器设备
#### 接口描述
新增一个呼叫器设备及其按钮配置。一个呼叫器设备可以配置多个按钮,每个按钮可以绑定不同的任务。
#### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/api/vwed-calldevice/add`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| protocol | String | 是 | 协议类型 |
| brand | String | 是 | 品牌 |
| ip | String | 是 | IP地址 |
| port | Integer | 是 | 端口号 |
| device_name | String | 是 | 设备名称 |
| status | Integer | 否 | 状态(0:禁用,1:启用)默认为1 |
| slave_id | String | 否 | 从机ID默认为"1" |
| buttons | Array | 否 | 按钮配置列表 |
**buttons数组中的元素结构**
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| signal_name | String | 是 | 信号名称 |
| signal_type | Integer | 是 | 信号类型 1:按钮 2:灯 |
| signal_length | String | 是 | 信号长度 |
| register_address | String | 是 | 寄存器地址 |
| function_code | String | 否 | 功能码 |
| remark | String | 否 | 备注 |
| vwed_task_id | String | 否 | 按下灯亮绑定的VWED任务ID |
| long_vwed_task_id | String | 否 | 长按取消后触发VWED任务ID |
#### 请求示例
```json
{
"protocol": "TCP",
"brand": "艾智威",
"ip": "10.2.0.10",
"port": 5020,
"device_name": "呼叫器-A区",
"status": 1,
"slave_id": "1",
"buttons": [
{
"signal_name": "button1",
"signal_type": 1,
"signal_length": "1",
"register_address": "1",
"function_code": "6",
"remark": "A区1号按钮",
"vwed_task_id": "193f8412-fa0d-4b6d-9648-70bceacd6629",
"long_vwed_task_id": "734749d1-0fdf-4a06-9fb7-8f991953d5e5"
},
{
"signal_name": "led1",
"signal_type": 2,
"signal_length": "1",
"register_address": "2",
"function_code": "6",
"remark": "A区1号指示灯",
"vwed_task_id": "7c6aac36-652b-4314-9433-f5788e9e7adb"
}
]
}
```
#### 响应参数
```json
{
"code": 200,
"message": "新增呼叫器设备成功",
"data": {
"id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"protocol": "TCP",
"brand": "艾智威",
"ip": "10.2.0.10",
"port": 5020,
"device_name": "呼叫器-A区",
"status": 1,
"slave_id": "1",
"buttons": [
{
"id": "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"signal_name": "button1",
"signal_type": 1,
"signal_length": "1",
"register_address": "1",
"function_code": "6",
"remark": "A区1号按钮",
"vwed_task_id": "193f8412-fa0d-4b6d-9648-70bceacd6629",
"long_vwed_task_id": "734749d1-0fdf-4a06-9fb7-8f991953d5e5"
},
{
"id": "6p5o4n3m-2l1k-0j9i-8h7g-6f5e4d3c2b1a",
"signal_name": "led1",
"signal_type": 2,
"signal_length": "1",
"register_address": "2",
"function_code": "6",
"remark": "A区1号指示灯",
"vwed_task_id": "7c6aac36-652b-4314-9433-f5788e9e7adb",
"long_vwed_task_id": null
}
]
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误如设备名称或IP地址已存在 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "设备名称 '呼叫器-A区' 已存在",
"data": null
}
```
```json
{
"code": 400,
"message": "IP地址 '10.2.0.10' 已存在",
"data": null
}
```
```json
{
"code": 400,
"message": "以下任务ID不存在: 193f8412-fa0d-4b6d-9648-70bceacd6629, 734749d1-0fdf-4a06-9fb7-8f991953d5e5",
"data": null
}
```
### 2. 更新呼叫器设备
#### 接口描述
更新指定ID的呼叫器设备信息及其按钮配置。更新时会替换所有原有的按钮配置。
#### 请求方式
- **HTTP方法**: PUT
- **接口路径**: `/api/vwed-calldevice/update/{device_id}`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| device_id | String | 是 | 设备ID路径参数 |
| protocol | String | 是 | 协议类型 |
| brand | String | 是 | 品牌 |
| ip | String | 是 | IP地址 |
| port | Integer | 是 | 端口号 |
| device_name | String | 是 | 设备名称 |
| status | Integer | 否 | 状态(0:禁用,1:启用)默认为1 |
| slave_id | String | 否 | 从机ID默认为"1" |
| buttons | Array | 否 | 按钮配置列表 |
**buttons数组中的元素结构**
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| signal_name | String | 是 | 信号名称 |
| signal_type | Integer | 是 | 信号类型 1:按钮 2:灯 |
| signal_length | String | 是 | 信号长度 |
| register_address | String | 是 | 寄存器地址 |
| function_code | String | 否 | 功能码 |
| remark | String | 否 | 备注 |
| vwed_task_id | String | 否 | 按下灯亮绑定的VWED任务ID |
| long_vwed_task_id | String | 否 | 长按取消后触发VWED任务ID |
#### 请求示例
```json
{
"protocol": "TCP",
"brand": "艾智威-更新",
"ip": "10.2.0.11",
"port": 5021,
"device_name": "呼叫器-A区-更新",
"status": 1,
"slave_id": "1",
"buttons": [
{
"signal_name": "button1",
"signal_type": 1,
"signal_length": "1",
"register_address": "1",
"function_code": "6",
"remark": "A区1号按钮-更新",
"vwed_task_id": "193f8412-fa0d-4b6d-9648-70bceacd6629",
"long_vwed_task_id": "734749d1-0fdf-4a06-9fb7-8f991953d5e5"
},
{
"signal_name": "led1",
"signal_type": 2,
"signal_length": "1",
"register_address": "2",
"function_code": "6",
"remark": "A区1号指示灯-更新",
"vwed_task_id": "7c6aac36-652b-4314-9433-f5788e9e7adb"
},
{
"signal_name": "button2",
"signal_type": 1,
"signal_length": "1",
"register_address": "3",
"function_code": "6",
"remark": "A区2号按钮-新增",
"vwed_task_id": "9a8b7c6d-5e4f-3g2h-1i0j-9k8l7m6n5o4p"
}
]
}
```
#### 响应参数
```json
{
"code": 200,
"message": "更新呼叫器设备成功",
"data": {
"id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"protocol": "TCP",
"brand": "艾智威-更新",
"ip": "10.2.0.11",
"port": 5021,
"device_name": "呼叫器-A区-更新",
"status": 1,
"slave_id": "1",
"buttons": [
{
"id": "2b3c4d5e-6f7g-8h9i-0j1k-2l3m4n5o6p7q",
"signal_name": "button1",
"signal_type": 1,
"signal_length": "1",
"register_address": "1",
"function_code": "6",
"remark": "A区1号按钮-更新",
"vwed_task_id": "193f8412-fa0d-4b6d-9648-70bceacd6629",
"long_vwed_task_id": "734749d1-0fdf-4a06-9fb7-8f991953d5e5"
},
{
"id": "7q6p5o4n-3m2l-1k0j-9i8h-7g6f5e4d3c2b",
"signal_name": "led1",
"signal_type": 2,
"signal_length": "1",
"register_address": "2",
"function_code": "6",
"remark": "A区1号指示灯-更新",
"vwed_task_id": "7c6aac36-652b-4314-9433-f5788e9e7adb",
"long_vwed_task_id": null
},
{
"id": "8r7q6p5o-4n3m-2l1k-0j9i-8h7g6f5e4d3c",
"signal_name": "button2",
"signal_type": 1,
"signal_length": "1",
"register_address": "3",
"function_code": "6",
"remark": "A区2号按钮-新增",
"vwed_task_id": "9a8b7c6d-5e4f-3g2h-1i0j-9k8l7m6n5o4p",
"long_vwed_task_id": null
}
]
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误如设备不存在、IP地址已被其他设备使用 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "未找到ID为 'modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f' 的呼叫器设备",
"data": null
}
```
```json
{
"code": 400,
"message": "IP地址 '10.2.0.11' 已被其他设备使用",
"data": null
}
```
```json
{
"code": 400,
"message": "以下任务ID不存在: 9a8b7c6d-5e4f-3g2h-1i0j-9k8l7m6n5o4p",
"data": null
}
```
### 4. 删除呼叫器设备
#### 接口描述
批量删除多个呼叫器设备及其所有按钮配置(软删除)。
#### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/api/vwed-calldevice/batch-delete`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| ids | Array | 是 | 要删除的设备ID列表 |
#### 请求示例
```json
{
"ids": [
"modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"modbus_device_1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p"
]
}
```
#### 响应参数
```json
{
"code": 200,
"message": "成功删除 2 个呼叫器设备",
"data": {
"ids": [
"modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"modbus_device_1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p"
]
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误如设备ID不存在或ID列表为空 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "设备ID列表不能为空",
"data": null
}
```
```json
{
"code": 400,
"message": "未找到以下ID的呼叫器设备: modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f, modbus_device_1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"data": null
}
```
### 5. 获取呼叫器设备列表
#### 接口描述
获取系统中的呼叫器设备列表支持分页、设备名称搜索、IP地址搜索和状态过滤。
#### 请求方式
- **HTTP方法**: GET
- **接口路径**: `/api/vwed-calldevice/list`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| page | Integer | 否 | 页码默认为1 |
| page_size | Integer | 否 | 每页数量默认为10最大100 |
| device_name | String | 否 | 设备名称搜索 |
| ip | String | 否 | IP地址搜索 |
| protocol | String | 否 | 协议类型搜索 |
| status | Integer | 否 | 状态过滤(0:禁用,1:启用) |
#### 请求示例
```
GET /api/vwed-calldevice/list?page=1&page_size=10&device_name=呼叫器&ip=10.2&protocol=TCP
```
#### 响应参数
```json
{
"code": 200,
"message": "获取呼叫器设备列表成功",
"data": {
"list": [
{
"id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"protocol": "TCP",
"brand": "艾智威",
"ip": "10.2.0.10",
"port": 5020,
"device_name": "呼叫器-A区",
"status": 1,
"slave_id": "1",
"created_at": "2023-06-15 10:30:45",
"button_count": 2
},
{
"id": "modbus_device_1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"protocol": "TCP",
"brand": "艾智威",
"ip": "10.2.0.11",
"port": 5021,
"device_name": "呼叫器-B区",
"status": 1,
"slave_id": "2",
"created_at": "2023-06-14 14:20:30",
"button_count": 3
}
],
"pagination": {
"total": 5,
"page": 1,
"page_size": 10,
"total_pages": 1
}
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "获取呼叫器设备列表失败: 参数错误",
"data": null
}
```
### 6. 获取呼叫器设备详情
#### 接口描述
根据设备ID获取呼叫器设备的详细信息包括设备基本信息和按钮配置。
#### 请求方式
- **HTTP方法**: GET
- **接口路径**: `/api/vwed-calldevice/detail/{device_id}`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| device_id | String | 是 | 设备ID路径参数 |
#### 请求示例
```
GET /api/vwed-calldevice/detail/modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f
```
#### 响应参数
```json
{
"code": 200,
"message": "获取呼叫器设备详情成功",
"data": {
"id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"protocol": "TCP",
"brand": "艾智威",
"ip": "10.2.0.10",
"port": 5020,
"device_name": "呼叫器-A区",
"status": 1,
"slave_id": "1",
"created_at": "2023-06-15 10:30:45",
"updated_at": "2023-06-16 09:15:20",
"buttons": [
{
"id": "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"signal_name": "button1",
"signal_type": 1,
"signal_length": "1",
"register_address": "1",
"function_code": "6",
"remark": "A区1号按钮",
"vwed_task_id": "193f8412-fa0d-4b6d-9648-70bceacd6629",
"long_vwed_task_id": "734749d1-0fdf-4a06-9fb7-8f991953d5e5",
"created_at": "2023-06-15 10:30:45"
},
{
"id": "6p5o4n3m-2l1k-0j9i-8h7g-6f5e4d3c2b1a",
"signal_name": "led1",
"signal_type": 2,
"signal_length": "1",
"register_address": "2",
"function_code": "6",
"remark": "A区1号指示灯",
"vwed_task_id": "7c6aac36-652b-4314-9433-f5788e9e7adb",
"long_vwed_task_id": null,
"created_at": "2023-06-15 10:30:45"
}
]
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误如设备ID不存在 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "未找到ID为 'modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f' 的呼叫器设备",
"data": null
}
```
### 7. 批量导出呼叫器设备
#### 接口描述
将选定的多个呼叫器设备导出为加密格式的文件,便于跨系统迁移或备份。
#### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/api/vwed-calldevice/export-batch`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| ids | Array | 是 | 要导出的设备ID列表 |
#### 请求示例
```json
{
"ids": [
"modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"modbus_device_1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p"
]
}
```
#### 响应参数
文件下载响应,包含加密的设备配置文件。
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误如设备ID不存在或ID列表为空 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "未找到以下ID的呼叫器设备: modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f, modbus_device_1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"data": null
}
```
### 8. 导入呼叫器设备
#### 接口描述
从加密的配置文件导入呼叫器设备配置,支持同时导入多个设备。导入的设备名称会自动添加后缀以避免与已有设备冲突。
#### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/api/vwed-calldevice/import`
#### 请求参数
表单提交,包含文件字段:
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| file | File | 是 | 呼叫器设备配置文件,加密专有格式 |
#### 响应参数
```json
{
"code": 200,
"message": "成功导入 2 个呼叫器设备",
"data": {
"success_count": 2,
"failed_count": 0,
"failed_devices": [],
"imported_devices": [
{
"id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"device_name": "呼叫器-A区-备份-168332148391872",
"original_name": "呼叫器-A区"
},
{
"id": "modbus_device_1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"device_name": "呼叫器-B区-备份-168332148391872",
"original_name": "呼叫器-B区"
}
]
}
}
```
#### 部分导入成功示例
```json
{
"code": 200,
"message": "部分导入成功: 成功 1 个, 失败 1 个",
"data": {
"success_count": 1,
"failed_count": 1,
"failed_devices": [
{
"index": 1,
"device_name": "呼叫器-B区-备份-168332148391872",
"reason": "IP地址 '10.2.0.11' 已存在"
}
],
"imported_devices": [
{
"id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"device_name": "呼叫器-A区-备份-168332148391872",
"original_name": "呼叫器-A区"
}
]
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误,如文件格式不正确或无法解析 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "无法导入呼叫器设备: 文件格式无效或已损坏",
"data": null
}
```
### 9. 启动呼叫器
#### 接口描述
启动呼叫器。开启一个监控线程,持续监控设备按钮状态,当检测到按钮按下时触发对应任务。
#### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/api/vwed-calldevice/monitor/start/{device_id}`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| device_id | String | 是 | 设备ID路径参数 |
#### 请求示例
```
POST /api/vwed-calldevice/monitor/start/modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f
```
#### 响应参数
```json
{
"code": 200,
"message": "启动设备监控成功",
"data": {
"device_id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"device_name": "呼叫器-A区",
"monitor_status": "running",
"started_at": "2023-06-18 10:15:30"
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误如设备ID不存在或设备已在监控中 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "启动设备监控失败: 未找到ID为 'modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f' 的呼叫器设备",
"data": null
}
```
```json
{
"code": 400,
"message": "启动设备监控失败: 设备监控已在运行中",
"data": null
}
```
### 10. 停止呼叫器
#### 接口描述
停止呼叫器监控
#### 请求方式
- **HTTP方法**: POST
- **接口路径**: `/api/vwed-calldevice/monitor/stop/{device_id}`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| device_id | String | 是 | 设备ID路径参数 |
#### 请求示例
```
POST /api/vwed-calldevice/monitor/stop/modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f
```
#### 响应参数
```json
{
"code": 200,
"message": "停止设备监控成功",
"data": {
"device_id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"device_name": "呼叫器-A区",
"monitor_status": "stopped",
"stopped_at": "2023-06-18 11:30:45"
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误如设备ID不存在或设备未在监控中 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "停止设备监控失败: 未找到ID为 'modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f' 的呼叫器设备",
"data": null
}
```
```json
{
"code": 400,
"message": "停止设备监控失败: 设备监控未运行",
"data": null
}
```
### 11. 获取设备监控状态
#### 接口描述
获取指定设备的监控状态信息。
#### 请求方式
- **HTTP方法**: GET
- **接口路径**: `/api/vwed-calldevice/monitor/status/{device_id}`
#### 请求参数
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|-----|------|
| device_id | String | 是 | 设备ID路径参数 |
#### 请求示例
```
GET /api/vwed-calldevice/monitor/status/modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f
```
#### 响应参数
```json
{
"code": 200,
"message": "获取设备监控状态成功",
"data": {
"device_id": "modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f",
"device_name": "呼叫器-A区",
"monitor_status": "running",
"started_at": "2023-06-18 10:15:30",
"uptime_seconds": 4500,
"button_triggers": {
"total": 15,
"details": [
{
"button_address": "1",
"trigger_count": 8,
"last_triggered_at": "2023-06-18 11:20:45"
},
{
"button_address": "2",
"trigger_count": 7,
"last_triggered_at": "2023-06-18 11:15:30"
}
]
}
}
}
```
#### 错误码
| 错误码 | 描述 |
|-------|------|
| 400 | 参数错误如设备ID不存在 |
| 500 | 服务器内部错误 |
#### 错误响应示例
```json
{
"code": 400,
"message": "获取设备监控状态失败: 未找到ID为 'modbus_device_8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f' 的呼叫器设备",
"data": null
}
```

View File

@ -0,0 +1,577 @@
# 地图数据推送接口文档
## 概述
地图数据推送接口用于处理用户推送新地图时的动作点和库区数据存储。支持库区分类(密集库区、一般库区)和动作点分层存储功能。
**重要特性:** 采用智能覆盖模式,先查询数据库是否有相关场景数据:
- 如果有现有数据,则先删除现有数据,然后创建新数据
- 如果没有现有数据,则直接创建新数据
- 确保数据的完整性和一致性
- 支持动作站点名称和库位名称的重复检查,自动过滤重复数据
## 接口列表
### 1. 推送地图数据
**接口地址:** `POST /api/vwed-map-data/push`
**功能说明:** 当用户推送新的地图时,将地图相关的动作点和库区数据存入数据库。支持库区分类和动作点分层存储。每个动作点包含站点名称和库位名称两个唯一标识。
**重要说明:** 采用智能覆盖模式,先查询数据库是否有相关场景数据,如果有则先删除现有数据再创建,如果没有则直接创建。自动检查并过滤重复的动作点。
**请求参数:**
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| scene_id | string | 是 | 场景ID |
| storage_areas | array | 是 | 库区数据列表 |
| operate_points | array | 是 | 动作点数据列表 |
**库区数据结构storage_areas**
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| id | string | 是 | 库区ID |
| area_name | string | 是 | 库区名称 |
| area_code | string | 是 | 库区编码 |
| area_type | string | 是 | 库区类型general-一般库区dense-密集库区) |
| description | string | 否 | 库区描述 |
| tags | string | 否 | 库区标签 |
**注意:** 库区的最大容量max_capacity由系统根据库区类型和包含的动作点数量自动计算无需手动指定。
**动作点数据结构operate_points**
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| station_name | string | 是 | 动作站点名称(唯一标识) |
| storage_location_name | string | 是 | 库位名称(唯一标识) |
| storage_area_id | string | 否 | 所属库区ID |
| max_layers | integer | 否 | 最大层数默认1 |
| position_x | integer | 否 | X坐标 |
| position_y | integer | 否 | Y坐标 |
| position_z | integer | 否 | Z坐标 |
| content | string | 否 | 内容 |
| tags | string | 否 | 标签 |
| description | string | 否 | 动作点描述 |
| layers | array | 否 | 分层数据 |
**注意:** 动作点ID由系统自动生成UUID无需手动指定。站点名称和库位名称作为唯一标识同一场景下不能重复。
**分层数据结构layers**
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| layer_index | integer | 是 | 层索引从1开始 |
| layer_name | string | 否 | 层名称 |
| max_weight | integer | 否 | 最大承重(克) |
| max_volume | integer | 否 | 最大体积(立方厘米) |
| layer_height | integer | 否 | 层高(毫米) |
| description | string | 否 | 层描述 |
| tags | string | 否 | 层标签 |
**请求示例:**
```json
{
"scene_id": "scene-001",
"storage_areas": [
{
"id": "area-002",
"area_name": "一般存储区B",
"area_code": "GENERAL-B",
"area_type": "general"
}
],
"operate_points": [
{
"station_name": "STATION-A-001",
"storage_location_name": "库位A001",
"storage_area_id": "area-002",
"max_layers": 2,
"layers": [
{
"layer_index": 1,
"layer_name": "第1层"
},
{
"layer_index": 2,
"layer_name": "第2层"
}
]
},
{
"station_name": "STATION-B-001",
"storage_location_name": "库位B002",
"storage_area_id": "area-002",
"max_layers": 1,
"layers": [
{
"layer_index": 1,
"layer_name": "第1层"
}
]
},
{
"station_name": "STATION-B-003",
"storage_location_name": "库位B003",
"max_layers": 1,
"layers": [
{
"layer_index": 1,
"layer_name": "第1层"
}
]
}
]
}
```
**响应参数:**
| 参数名 | 类型 | 描述 |
|--------|------|------|
| code | integer | 状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
**响应数据结构data**
| 参数名 | 类型 | 描述 |
|--------|------|------|
| scene_id | string | 场景ID |
| storage_areas_count | integer | 创建的库区数量 |
| operate_points_count | integer | 创建的动作点数量 |
| layers_count | integer | 创建的分层数量 |
| message | string | 推送结果说明 |
**响应示例:**
**覆盖现有数据时:**
```json
{
"code": 200,
"message": "地图数据推送成功",
"data": {
"scene_id": "scene-001",
"storage_areas_count": 2,
"operate_points_count": 2,
"layers_count": 2,
"message": "推送成功已覆盖现有数据。创建了2个库区2个动作点2个分层"
}
}
```
**创建新数据时:**
```json
{
"code": 200,
"message": "地图数据推送成功",
"data": {
"scene_id": "scene-002",
"storage_areas_count": 2,
"operate_points_count": 2,
"layers_count": 2,
"message": "推送成功创建了2个库区2个动作点2个分层"
}
}
```
**有重复数据被过滤时:**
```json
{
"code": 200,
"message": "地图数据推送成功",
"data": {
"scene_id": "scene-001",
"storage_areas_count": 2,
"operate_points_count": 3,
"layers_count": 4,
"message": "推送成功创建了2个库区3个动作点4个分层。检测到2个重复的站点名称已被过滤STATION-A-001, STATION-B-001"
}
}
```
**错误响应示例:**
```json
{
"code": 400,
"message": "动作站点名称不能重复",
"data": null
}
```
```json
{
"code": 400,
"message": "库位名称不能重复",
"data": null
}
```
**智能覆盖模式说明:**
- 先查询数据库是否有指定场景的现有数据
- 如果有现有数据,则先删除所有现有数据(库区、动作点、分层),然后创建新数据
- 如果没有现有数据,则直接创建新数据,提高处理效率
- 不支持增量更新,只支持全量覆盖
- 建议在推送前确认数据的完整性,避免数据丢失
**重复检查机制:**
- 同一场景下动作站点名称station_name不能重复
- 同一场景下库位名称storage_location_name不能重复
- 系统会自动过滤重复的动作点,并在响应中提示被过滤的数量和名称
- 支持与数据库现有数据的重复检查和请求中的重复检查
---
### 2. 查询地图数据
**接口地址:** `POST /api/vwed-map-data/query`
**功能说明:** 根据场景ID查询地图中的库区和动作点数据支持按库区类型筛选和是否包含分层数据的选项。
**请求参数:**
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| scene_id | string | 是 | 场景ID |
| area_type | string | 否 | 库区类型筛选general/dense |
| include_layers | boolean | 否 | 是否包含分层数据默认true |
**请求示例:**
```json
{
"scene_id": "scene-001",
"area_type": "dense",
"include_layers": true
}
```
**响应参数:**
| 参数名 | 类型 | 描述 |
|--------|------|------|
| code | integer | 状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
**响应数据结构data**
| 参数名 | 类型 | 描述 |
|--------|------|------|
| scene_id | string | 场景ID |
| storage_areas | array | 库区数据列表 |
| operate_points | array | 动作点数据列表 |
| total_capacity | integer | 总容量 |
| used_capacity | integer | 已使用容量 |
| dense_areas_count | integer | 密集库区数量 |
| general_areas_count | integer | 一般库区数量 |
| total_layers | integer | 总分层数量 |
| occupied_layers | integer | 已占用分层数量 |
**响应示例:**
```json
{
"code": 200,
"message": "查询成功",
"data": {
"scene_id": "scene-001",
"storage_areas": [
{
"id": "area-001",
"area_name": "密集存储区A",
"area_code": "DENSE-A",
"area_type": "dense",
"scene_id": "scene-001",
"max_capacity": 100,
"current_usage": 0,
"is_active": true,
"is_maintenance": false,
"description": "高密度存储区域,适合小件物品",
"tags": "dense,small-items",
"created_at": "2025-01-09 10:00:00",
"updated_at": "2025-01-09 10:00:00",
"is_deleted": false
}
],
"operate_points": [
{
"id": "point-001",
"station_name": "STATION-A-001",
"scene_id": "scene-001",
"is_occupied": false,
"is_locked": false,
"is_disabled": false,
"is_empty_tray": false,
"content": "存储点1",
"tags": "storage",
"storage_area_id": "area-001",
"storage_area_type": "dense",
"area_name": "密集存储区A",
"max_layers": 2,
"current_layers": 0,
"position_x": 100,
"position_y": 200,
"position_z": 0,
"description": "密集区第一个动作点",
"layers": [
{
"id": "layer-001",
"operate_point_id": "point-001",
"layer_index": 1,
"layer_name": "第1层",
"has_goods": false,
"goods_content": "",
"goods_weight": null,
"goods_volume": null,
"is_locked": false,
"is_disabled": false,
"is_reserved": false,
"max_weight": 5000,
"max_volume": 1000,
"layer_height": 100,
"goods_stored_at": null,
"goods_retrieved_at": null,
"last_access_at": null,
"tags": "bottom",
"description": "底层存储",
"created_at": "2025-01-09 10:00:00",
"updated_at": "2025-01-09 10:00:00",
"is_deleted": false
},
{
"id": "layer-002",
"operate_point_id": "point-001",
"layer_index": 2,
"layer_name": "第2层",
"has_goods": false,
"goods_content": "",
"goods_weight": null,
"goods_volume": null,
"is_locked": false,
"is_disabled": false,
"is_reserved": false,
"max_weight": 5000,
"max_volume": 1000,
"layer_height": 100,
"goods_stored_at": null,
"goods_retrieved_at": null,
"last_access_at": null,
"tags": "top",
"description": "顶层存储",
"created_at": "2025-01-09 10:00:00",
"updated_at": "2025-01-09 10:00:00",
"is_deleted": false
}
],
"created_at": "2025-01-09 10:00:00",
"updated_at": "2025-01-09 10:00:00",
"is_deleted": false
}
],
"total_capacity": 100,
"used_capacity": 0,
"dense_areas_count": 1,
"general_areas_count": 0,
"total_layers": 2,
"occupied_layers": 0
}
}
```
---
### 3. 删除场景数据
**接口地址:** `DELETE /api/vwed-map-data/scene/{scene_id}`
**功能说明:** 删除指定场景的所有地图数据,包括库区、动作点和分层数据。
**路径参数:**
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| scene_id | string | 是 | 场景ID |
**请求示例:**
```
DELETE /api/vwed-map-data/scene/scene-001
```
**响应示例:**
```json
{
"code": 200,
"message": "场景数据删除成功",
"data": null
}
```
---
### 4. 获取场景数据摘要
**接口地址:** `GET /api/vwed-map-data/scene/{scene_id}/summary`
**功能说明:** 获取指定场景的数据统计信息,包括库区和动作点的数量统计。
**路径参数:**
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| scene_id | string | 是 | 场景ID |
**请求示例:**
```
GET /api/vwed-map-data/scene/scene-001/summary
```
**响应数据结构data**
| 参数名 | 类型 | 描述 |
|--------|------|------|
| scene_id | string | 场景ID |
| total_storage_areas | integer | 总库区数量 |
| total_operate_points | integer | 总动作点数量 |
| total_capacity | integer | 总容量 |
| used_capacity | integer | 已使用容量 |
| capacity_usage_rate | float | 容量使用率(百分比) |
| dense_areas_count | integer | 密集库区数量 |
| general_areas_count | integer | 一般库区数量 |
| total_layers | integer | 总分层数量 |
| occupied_layers | integer | 已占用分层数量 |
| layer_usage_rate | float | 分层使用率(百分比) |
**响应示例:**
```json
{
"code": 200,
"message": "获取场景摘要成功",
"data": {
"scene_id": "scene-001",
"total_storage_areas": 2,
"total_operate_points": 2,
"total_capacity": 150,
"used_capacity": 45,
"capacity_usage_rate": 30.0,
"dense_areas_count": 1,
"general_areas_count": 1,
"total_layers": 3,
"occupied_layers": 1,
"layer_usage_rate": 33.33
}
}
```
## 数据验证规则
### 1. 库区数据验证
- 库区ID、名称、编码不能重复
- 库区类型必须为 "general" 或 "dense"
- 最大容量不能为负数
### 2. 动作点数据验证
- 站点名称不能重复(作为唯一标识)
- 最大层数必须大于0
- 如果指定库区ID必须在库区列表中存在
- 动作点ID由系统自动生成UUID无需手动设置
- 库区类型字段在地图推送时自动填充,无需手动设置
- 库区名称字段在地图推送时自动填充,无需手动设置
### 3. 分层数据验证
- 层索引必须从1开始且不能重复
- 层索引不能超过动作点的最大层数
- 分层数量不能超过最大层数
- 最大承重、最大体积、层高不能为负数
## 状态码说明
| 状态码 | 说明 |
|--------|------|
| 200 | 请求成功 |
| 400 | 请求参数错误或数据验证失败 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
## 使用说明
### 1. 推送地图数据流程
1. 准备库区数据,包括库区类型(密集/一般)、容量等信息
2. 准备动作点数据,包括位置坐标、所属库区等信息
3. 如果动作点支持分层,准备分层数据,包括层索引、承重等信息
4. 调用推送接口,系统会自动处理数据关联和验证
5. 如果需要覆盖现有数据,设置 `is_override``true`
### 2. 查询地图数据流程
1. 指定场景ID进行查询
2. 可选择性筛选库区类型(密集/一般)
3. 可选择是否包含分层详细数据
4. 系统返回完整的地图数据和统计信息
### 3. 分层存储支持
- 每个动作点可以设置最大层数
- 每层可以独立设置承重、体积等限制
- 支持货物存取状态追踪
- 支持分层锁定和预留功能
### 4. 库区类型说明
- **密集库区dense**:适合小件物品的高密度存储
- **一般库区general**:适合大件物品的常规存储
### 5. 库区容量自动计算规则
系统会根据以下规则自动计算库区的最大容量:
#### 密集库区dense
- 基础容量50可通过环境变量 `MAP_DENSE_STORAGE_BASE_CAPACITY` 配置)
- 每个动作点增加容量10可通过环境变量 `MAP_DENSE_STORAGE_CAPACITY_PER_POINT` 配置)
- 分层倍数1.5(可通过环境变量 `MAP_DENSE_STORAGE_LAYER_MULTIPLIER` 配置)
#### 一般库区general
- 基础容量30可通过环境变量 `MAP_GENERAL_STORAGE_BASE_CAPACITY` 配置)
- 每个动作点增加容量15可通过环境变量 `MAP_GENERAL_STORAGE_CAPACITY_PER_POINT` 配置)
- 分层倍数1.2(可通过环境变量 `MAP_GENERAL_STORAGE_LAYER_MULTIPLIER` 配置)
#### 计算公式
```
库区容量 = 基础容量 + Σ(动作点容量)
动作点容量 = 基础点容量 × (层数 > 1 ? 分层倍数 × 层数 : 1)
```
#### 示例(基于默认配置)
- 密集库区包含2个单层动作点50 + 10 + 10 = 70
- 密集库区包含1个双层动作点50 + (10 × 1.5 × 2) = 80
- 一般库区包含1个三层动作点30 + (15 × 1.2 × 3) = 84
#### 配置说明
系统支持通过环境变量动态调整库区容量计算参数,便于在不同环境中灵活配置。
### 6. 注意事项
- 动作点ID由系统自动生成32位UUID字符串无需手动设置
- 库区ID字段使用32位字符串建议使用UUID格式
- 站点名称作为动作点的唯一标识,不能重复
- 坐标系统使用整数,单位建议为毫米
- 重量单位为克,体积单位为立方厘米
- 软删除机制,删除操作不会物理删除数据
- 支持事务处理,确保数据一致性
- 库区容量由系统自动计算,会在推送时根据动作点配置重新计算
- 库区类型字段在地图推送时自动填充到动作点中,便于快速查询动作点所属库区类型
- 库区名称字段在地图推送时自动填充到动作点中,便于快速查询动作点所属库区名称

View File

@ -695,4 +695,476 @@
"message": "设置库位标签失败: 库位不存在",
"code": 404
}
```
```
## 获取库位详情 (GetStorageLocationDetail)
本接口用于获取指定库位的详细信息。
### 描述
本接口用于获取指定库位的完整详情信息,包括库位基本信息、动作点信息、扩展字段定义和值、状态变更历史等。适用于需要全面了解库位信息的场景。
### 接口信息
- **URL**: `/api/vwed-operate-point/{storage_location_id}`
- **方法**: GET
- **认证**: 需要
### 输入参数
| 参数名 | 中文名称 | 是否必填 | 类型 | 描述 |
| ------------------ | -------- | -------- | ------ | ------------------------------ |
| storage_location_id | 库位ID | 必填 | String | 要获取详情的库位ID |
### 输出参数
| 参数名 | 中文名称 | 类型 | 描述 |
| -------------------------- | --------------- | ------- | ------------------------------ |
| code | 返回代码 | Number | 操作结果代码 |
| message | 消息 | String | 操作结果消息 |
| data | 数据 | Object | 返回数据对象 |
| data.storage_location | 库位信息 | Object | 库位基本信息 |
| data.operate_point_info | 动作点信息 | Object | 所属动作点的详细信息 |
| data.extended_fields_definitions | 扩展字段定义 | Array | 扩展字段的定义信息 |
| data.status_history | 状态变更历史 | Array | 库位状态变更历史记录 |
### 调用示例
```json
// 请求URL
GET /api/vwed-operate-point/layer-001
// 成功响应示例
{
"code": 200,
"message": "获取库位详情成功",
"data": {
"storage_location": {
"id": "layer-001",
"layer_index": 1,
"layer_name": "第一层",
"is_occupied": false,
"is_locked": false,
"is_disabled": false,
"is_empty_tray": false,
"locked_by": null,
"goods_content": "",
"goods_weight": null,
"goods_volume": null,
"goods_stored_at": null,
"goods_retrieved_at": null,
"last_access_at": null,
"max_weight": 5000,
"max_volume": 1000,
"layer_height": 300,
"tags": "高架库,密集存储",
"description": "密集存储区第一层",
"created_at": "2024-01-01T00:00:00",
"updated_at": "2024-01-01T00:00:00",
"extended_fields": {
"产品类别": "电子产品",
"存储温度": "常温"
},
"operate_point_id": "point-001",
"station_name": "存储站点A",
"storage_location_name": "A1库位",
"scene_id": "scene-001",
"storage_area_id": "area-001"
},
"operate_point_info": {
"id": "point-001",
"station_name": "存储站点A",
"storage_location_name": "A1库位",
"scene_id": "scene-001",
"storage_area_id": "area-001",
"storage_area_type": "dense",
"area_name": "密集存储区",
"max_layers": 10,
"current_layers": 5,
"position_x": 100,
"position_y": 200,
"position_z": 0,
"description": "密集存储区的动作点",
"created_at": "2024-01-01T00:00:00",
"updated_at": "2024-01-01T00:00:00",
"is_deleted": false
},
"extended_fields_definitions": [
{
"id": "ext-001",
"property_name": "产品类别",
"property_type": "select",
"is_required": true,
"is_enabled": true,
"description": "产品分类",
"options": [
{"value": "电子产品", "label": "电子产品"},
{"value": "机械配件", "label": "机械配件"}
]
},
{
"id": "ext-002",
"property_name": "存储温度",
"property_type": "string",
"is_required": false,
"is_enabled": true,
"description": "存储温度要求"
}
],
"status_history": []
}
}
// 失败响应示例
{
"code": 404,
"message": "库位 layer-001 不存在",
"data": null
}
```
## 编辑库位信息 (EditStorageLocation)
本接口用于编辑和更新库位的各种属性信息。
### 描述
本接口用于编辑库位的各种属性,包括货物信息、库位规格、状态字段、扩展字段等。支持部分更新,只有传入的字段且值发生变化时才会被更新。扩展字段必须在系统中已定义且已启用。
### 接口信息
- **URL**: `/api/vwed-operate-point/{storage_location_id}`
- **方法**: PUT
- **认证**: 需要
### 输入参数
| 参数名 | 中文名称 | 是否必填 | 类型 | 描述 |
| ------------------ | ------------ | -------- | ------- | ------------------------------ |
| storage_location_id | 库位ID | 必填 | String | 要编辑的库位ID |
| goods_content | 货物内容 | 非必填 | String | 货物内容描述 |
| goods_weight | 货物重量 | 非必填 | Integer | 货物重量(克) |
| goods_volume | 货物体积 | 非必填 | Integer | 货物体积(立方厘米) |
| max_weight | 最大承重 | 非必填 | Integer | 最大承重(克) |
| max_volume | 最大体积 | 非必填 | Integer | 最大体积(立方厘米) |
| layer_height | 层高 | 非必填 | Integer | 层高(毫米) |
| is_locked | 是否锁定 | 非必填 | Boolean | 库位是否锁定 |
| is_disabled | 是否禁用 | 非必填 | Boolean | 库位是否禁用 |
| is_empty_tray | 是否空托盘 | 非必填 | Boolean | 库位是否为空托盘状态 |
| tags | 标签 | 非必填 | String | 库位标签 |
| description | 描述 | 非必填 | String | 库位描述 |
| extended_fields | 扩展字段 | 非必填 | Object | 扩展字段的键值对 |
### 输出参数
| 参数名 | 中文名称 | 类型 | 描述 |
| -------------------------- | --------------- | ------- | ------------------------------ |
| code | 返回代码 | Number | 操作结果代码 |
| message | 消息 | String | 操作结果消息 |
| data | 数据 | Object | 返回数据对象 |
| data.storage_location_id | 库位ID | String | 被编辑的库位ID |
| data.success | 成功标志 | Boolean | 编辑是否成功 |
| data.message | 操作消息 | String | 详细的操作结果消息 |
| data.updated_fields | 更新字段列表 | Array | 已更新的字段名称列表 |
| data.updated_storage_location | 更新后库位信息 | Object | 更新后的库位完整信息 |
### 调用示例
```json
// 请求URL
PUT /api/vwed-operate-point/layer-001
// 请求体示例
{
"goods_content": "电子产品A",
"goods_weight": 500,
"goods_volume": 200,
"max_weight": 6000,
"is_locked": false,
"is_disabled": false,
"is_empty_tray": true,
"tags": "高架库,密集存储,已更新",
"description": "密集存储区第一层 - 已更新",
"extended_fields": {
"产品类别": "机械配件",
"存储温度": "常温",
"保质期": "12个月"
}
}
// 成功响应示例
{
"code": 200,
"message": "库位信息编辑成功",
"data": {
"storage_location_id": "layer-001",
"success": true,
"message": "库位信息更新成功,共更新 8 个字段",
"updated_fields": [
"goods_content",
"goods_weight",
"goods_volume",
"max_weight",
"is_locked",
"is_disabled",
"is_empty_tray",
"tags",
"description",
"extended_fields.产品类别",
"extended_fields.存储温度",
"extended_fields.保质期",
"updated_at"
],
"updated_storage_location": {
"id": "layer-001",
"layer_index": 1,
"layer_name": "第一层",
"is_occupied": false,
"is_locked": false,
"is_disabled": false,
"is_empty_tray": true,
"locked_by": null,
"goods_content": "电子产品A",
"goods_weight": 500,
"goods_volume": 200,
"goods_stored_at": null,
"goods_retrieved_at": null,
"last_access_at": null,
"max_weight": 6000,
"max_volume": 1000,
"layer_height": 300,
"tags": "高架库,密集存储,已更新",
"description": "密集存储区第一层 - 已更新",
"created_at": "2024-01-01T00:00:00",
"updated_at": "2024-01-02T10:30:00",
"extended_fields": {
"产品类别": "机械配件",
"存储温度": "常温",
"保质期": "12个月"
},
"operate_point_id": "point-001",
"station_name": "存储站点A",
"storage_location_name": "A1库位",
"scene_id": "scene-001",
"storage_area_id": "area-001"
}
}
}
// 失败响应示例
{
"code": 404,
"message": "库位 layer-001 不存在",
"data": null
}
// 没有字段变化的响应示例
{
"code": 200,
"message": "库位信息编辑成功",
"data": {
"storage_location_id": "layer-001",
"success": true,
"message": "没有字段发生变化,数据保持不变",
"updated_fields": [],
"updated_storage_location": {
"id": "layer-001",
"layer_index": 1,
"layer_name": "第一层",
"is_occupied": false,
"is_locked": false,
"is_disabled": false,
"is_empty_tray": false,
"locked_by": null,
"goods_content": "电子产品A",
"goods_weight": 500,
"goods_volume": 200,
"goods_stored_at": null,
"goods_retrieved_at": null,
"last_access_at": null,
"max_weight": 6000,
"max_volume": 1000,
"layer_height": 300,
"tags": "高架库,密集存储",
"description": "密集存储区第一层",
"created_at": "2024-01-01T00:00:00",
"updated_at": "2024-01-01T00:00:00",
"extended_fields": {
"产品类别": "电子产品",
"存储温度": "常温"
},
"operate_point_id": "point-001",
"station_name": "存储站点A",
"storage_location_name": "A1库位",
"scene_id": "scene-001",
"storage_area_id": "area-001"
}
}
}
// 部分字段更新失败示例
{
"code": 400,
"message": "更新扩展字段失败: 扩展字段 未定义字段 不存在或已禁用",
"data": null
}
```
### 注意事项
1. **部分更新**: 只有传入的字段且值发生变化时才会被更新,未传入的字段保持原值
2. **层名称限制**: 层名称layer_name不能通过此接口修改请使用其他方式
3. **状态字段支持**: 支持修改 `is_locked``is_disabled``is_empty_tray` 三个状态字段
4. **扩展字段验证**: 扩展字段必须在系统中已定义且已启用,否则会跳过更新
5. **数据变更校验**: 如果所有字段都没有发生变化,会返回相应提示信息
6. **字段验证**: 数值类型字段会进行范围验证,字符串类型字段会进行长度验证
7. **事务处理**: 所有更新操作在同一个事务中进行,确保数据一致性
## 获取库位操作记录 (GetStorageLocationOperationLogs)
本接口用于获取库位相关的操作记录,支持多种筛选条件和分页查询。
### 描述
本接口用于查询库位相关的操作记录,包括状态更新操作、信息编辑操作等。所有对库位的操作都会自动记录到操作日志中,方便后续审计和追踪。
### 接口信息
- **URL**: `/api/vwed-operate-point/operation-logs`
- **方法**: GET
- **认证**: 需要
### 输入参数
| 参数名 | 中文名称 | 是否必填 | 类型 | 描述 |
| ------------------ | ------------ | -------- | ------- | ------------------------------ |
| storage_location_id | 库位ID | 非必填 | String | 查询特定库位的操作记录 |
| operator | 操作人 | 非必填 | String | 操作人姓名(支持模糊搜索) |
| operation_type | 操作类型 | 非必填 | String | 操作类型occupy、release等|
| start_time | 开始时间 | 非必填 | String | 开始时间格式YYYY-MM-DD HH:MM:SS|
| end_time | 结束时间 | 非必填 | String | 结束时间格式YYYY-MM-DD HH:MM:SS|
| page | 页码 | 非必填 | Integer | 页码默认为1 |
| page_size | 每页数量 | 非必填 | Integer | 每页数量默认为20最大100 |
### 输出参数
| 参数名 | 中文名称 | 类型 | 描述 |
| -------------------------- | --------------- | ------- | ------------------------------ |
| code | 返回代码 | Number | 操作结果代码 |
| message | 消息 | String | 操作结果消息 |
| data | 数据 | Object | 返回数据对象 |
| data.total | 总数量 | Number | 符合条件的记录总数 |
| data.page | 当前页码 | Number | 当前页码 |
| data.page_size | 每页数量 | Number | 每页数量 |
| data.total_pages | 总页数 | Number | 总页数 |
| data.logs | 操作记录列表 | Array | 操作记录数组 |
| data.logs[].id | 记录ID | String | 操作记录的唯一标识 |
| data.logs[].operation_time | 操作时间 | String | 操作发生的时间 |
| data.logs[].operator | 操作人 | String | 执行操作的人员 |
| data.logs[].operation_type | 操作类型 | String | 操作类型 |
| data.logs[].affected_storage_locations | 影响的库位列表 | Array | 受影响的库位ID列表 |
| data.logs[].description | 操作描述 | String | 操作的详细描述 |
| data.logs[].created_at | 创建时间 | String | 记录创建时间 |
### 支持的操作类型
| 操作类型 | 中文名称 | 描述 |
| ------------ | ----------- | ------------------------------ |
| occupy | 占用库位 | 将库位设置为占用状态 |
| release | 释放库位 | 将库位设置为空闲状态 |
| lock | 锁定库位 | 锁定库位防止其他操作 |
| unlock | 解锁库位 | 解锁库位允许其他操作 |
| enable | 启用库位 | 启用库位使其可用 |
| disable | 禁用库位 | 禁用库位使其不可用 |
| set_empty_tray | 设置空托盘 | 将库位设置为空托盘状态 |
| clear_empty_tray | 清除空托盘 | 清除库位的空托盘状态 |
| 编辑库位 | 编辑库位 | 编辑库位的信息 |
### 调用示例
```json
// 请求URL示例
GET /api/vwed-operate-point/operation-logs?storage_location_id=layer-001&page=1&page_size=10
// 带时间范围的请求示例
GET /api/vwed-operate-point/operation-logs?start_time=2024-01-01 00:00:00&end_time=2024-01-02 23:59:59&operator=admin&page=1&page_size=20
// 成功响应示例
{
"code": 200,
"message": "查询操作记录成功",
"data": {
"total": 25,
"page": 1,
"page_size": 10,
"total_pages": 3,
"logs": [
{
"id": "log-001",
"operation_time": "2024-01-02T10:30:00",
"operator": "admin",
"operation_type": "编辑库位",
"affected_storage_locations": ["layer-001"],
"description": "编辑库位信息,更新字段: goods_content, goods_weight, is_locked",
"created_at": "2024-01-02T10:30:00"
},
{
"id": "log-002",
"operation_time": "2024-01-02T09:15:00",
"operator": "系统",
"operation_type": "occupy",
"affected_storage_locations": ["layer-001"],
"description": "占用库位操作",
"created_at": "2024-01-02T09:15:00"
},
{
"id": "log-003",
"operation_time": "2024-01-02T08:45:00",
"operator": "user001",
"operation_type": "lock",
"affected_storage_locations": ["layer-001", "layer-002"],
"description": "锁定库位操作",
"created_at": "2024-01-02T08:45:00"
}
]
}
}
// 空结果响应示例
{
"code": 200,
"message": "查询操作记录成功",
"data": {
"total": 0,
"page": 1,
"page_size": 10,
"total_pages": 0,
"logs": []
}
}
// 时间格式错误示例
{
"code": 400,
"message": "开始时间格式错误请使用格式YYYY-MM-DD HH:MM:SS",
"data": null
}
// 参数错误示例
{
"code": 400,
"message": "开始时间不能大于结束时间",
"data": null
}
```
### 注意事项
1. **时间格式**: 时间参数必须使用格式 `YYYY-MM-DD HH:MM:SS`,例如 `2024-01-01 12:00:00`
2. **时间范围**: 如果同时提供开始时间和结束时间,开始时间不能大于结束时间
3. **模糊搜索**: 操作人字段支持模糊搜索,会匹配包含关键词的记录
4. **排序方式**: 操作记录按操作时间倒序排列,最新的操作记录在前面
5. **自动记录**: 所有对库位的操作都会自动记录,无需手动创建记录
6. **分页限制**: 每页最多返回100条记录
7. **数据完整性**: 操作记录一旦创建就不能修改或删除,确保审计追踪的完整性

Binary file not shown.

202
app.py
View File

@ -1,29 +1,21 @@
# app.py
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
import logging
import time
import traceback
from utils.logger import get_logger
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
import uvicorn
import os
from contextlib import asynccontextmanager
# 导入配置
from config.settings import settings
from config.error_messages import VALIDATION_ERROR_MESSAGES, HTTP_ERROR_MESSAGES
# 导入数据库相关
from data.session import init_database, close_database_connections, close_async_database_connections
from data.cache import redis_client
# 引入路由
from routes.database import router as db_router
from routes.template_api import router as template_router
from routes.task_api import router as task_router
from routes.common_api import router as common_router, format_response
from routes.task_edit_api import router as task_edit_router
from routes.script_api import router as script_router
from routes.task_record_api import router as task_record_router
# from data.cache import redis_client
# 导入路由注册函数
from routes import register_routers
# 导入中间件注册函数
from middlewares import register_middlewares
# 导入日志工具
from utils.logger import get_logger
# 设置日志
logger = get_logger("app")
@ -37,8 +29,8 @@ async def lifespan(app: FastAPI):
# 初始化数据库
init_database()
# 初始化Redis连接
if redis_client.get_client() is None:
logger.warning("Redis连接失败部分功能可能无法正常使用")
# if redis_client.get_client() is None:
# logger.warning("Redis连接失败部分功能可能无法正常使用")
# 启动增强版任务调度器
from services.enhanced_scheduler import scheduler
@ -67,178 +59,26 @@ app = FastAPI(
debug=settings.DEBUG
)
# 添加CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=settings.CORS_ALLOW_CREDENTIALS,
allow_methods=settings.CORS_ALLOW_METHODS,
allow_headers=settings.CORS_ALLOW_HEADERS,
)
# 注册中间件
register_middlewares(app)
# 请求日志中间件
@app.middleware("http")
async def log_requests(request: Request, call_next):
"""记录请求日志的中间件"""
start_time = time.time()
# 获取请求信息
method = request.method
url = request.url.path
client_host = request.client.host if request.client else "unknown"
# 记录请求
logger.info(f"请求开始: {method} {url} 来自 {client_host}")
try:
# 处理请求
response = await call_next(request)
# 计算处理时间
process_time = time.time() - start_time
logger.info(f"请求完成: {method} {url} 状态码: {response.status_code} 耗时: {process_time:.4f}")
return response
except Exception as e:
# 记录异常
process_time = time.time() - start_time
logger.error(f"请求异常: {method} {url} 耗时: {process_time:.4f}")
logger.error(f"异常详情: {str(e)}")
logger.error(traceback.format_exc())
# 返回通用错误响应
return JSONResponse(
status_code=500,
content=format_response(
code=500,
message="服务器内部错误,请联系管理员",
data=None
)
)
# 注册所有路由
register_routers(app)
# 全局验证错误处理器
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
处理验证错误将错误消息转换为中文并提供更友好的错误提示
包括显示具体缺失的字段名称
"""
errors = exc.errors()
error_details = []
missing_fields = []
for error in errors:
error_type = error.get("type", "")
loc = error.get("loc", [])
# 获取完整的字段路径排除body/query等
if len(loc) > 1 and loc[0] in ["body", "query", "path", "header"]:
field_path = ".".join(str(item) for item in loc[1:])
else:
field_path = ".".join(str(item) for item in loc)
# 获取中文错误消息
message = VALIDATION_ERROR_MESSAGES.get(error_type, error.get("msg", "验证错误"))
# 替换消息中的参数
context = error.get("ctx", {})
for key, value in context.items():
message = message.replace(f"{{{key}}}", str(value))
# 收集缺失字段
if error_type == "missing" or error_type == "value_error.missing":
missing_fields.append(field_path)
error_details.append({
"field": field_path,
"message": message,
"type": error_type
})
# 构建友好的错误响应
if missing_fields:
missing_fields_str = ", ".join(missing_fields)
error_message = f"缺少必填字段: {missing_fields_str}"
elif error_details:
# 提取第一个错误的字段和消息
first_error = error_details[0]
error_message = f"参数 '{first_error['field']}' 验证失败: {first_error['message']}"
else:
error_message = "参数验证失败"
return JSONResponse(
status_code=400,
content={
"code": 400,
"message": error_message,
"data": error_details if len(error_details) > 1 else None
}
)
# HTTP错误处理器
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""处理HTTP异常转换为统一的响应格式"""
status_code = exc.status_code
# 获取错误消息,优先使用自定义消息,否则使用配置中的错误消息
message = exc.detail
if isinstance(message, str) and message == "Not Found":
message = HTTP_ERROR_MESSAGES.get(status_code, message)
return JSONResponse(
status_code=status_code,
content=format_response(
code=status_code,
message=message,
data=None
)
)
# 全局异常处理器
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""处理所有未捕获的异常"""
logger.error(f"未捕获异常: {str(exc)}")
logger.error(traceback.format_exc())
return JSONResponse(
status_code=500,
content=format_response(
code=500,
message="服务器内部错误,请联系管理员",
data=None if not settings.DEBUG else str(exc)
)
)
# 注册路由
app.include_router(common_router)
app.include_router(db_router)
app.include_router(template_router)
app.include_router(task_router)
app.include_router(task_edit_router)
app.include_router(script_router)
app.include_router(task_record_router)
# 根路由
@app.get("/")
async def root():
"""API根路由显示系统基本信息"""
return {
"app_name": settings.APP_NAME,
"version": settings.APP_VERSION,
"description": settings.APP_DESCRIPTION,
"status": "running"
}
# 主函数
if __name__ == "__main__":
# 从环境变量中获取端口默认为8000
port = int(os.environ.get("PORT", settings.SERVER_PORT))
# 打印启动配置信息
logger.info(f"服务器配置 - Host: 0.0.0.0, Port: {port}, Workers: {settings.SERVER_WORKERS}, Reload: {settings.SERVER_RELOAD}")
# 启动服务器
uvicorn.run(
"app:app",
host="0.0.0.0",
port=port,
reload=settings.DEBUG,
reload=settings.SERVER_RELOAD, # 使用SERVER_RELOAD而不是DEBUG
workers=settings.SERVER_WORKERS
)

Binary file not shown.

View File

@ -36,7 +36,7 @@
"options": []
}
],
"extraInputParamsFunc": "## 通用读取 Modbus 值(Name) (ModbusCommonReadNameBp)\n\n### 描述\n本块用于通过设备名称和地址读取Modbus设备的值。\n\n### 输入参数\n\n| 参数名 | 是否必填 | 类型 | 描述 |\n|-------------|---------|----------|------------|\n| instanceName | 必填 | String | Name |\n| address | 选填 | Integer | 地址编号 |\n| remark | 选填 | String | 地址说明 |\n\n### 上下文变量\n\n| 变量名 | 类型 | 描述 |\n|-------------|----------|------------|\n| modbusValue | String | Modbus值 |\n\n### 输出参数\n无",
"extraInputParamsFunc": "",
"outputParams": {},
"contextVariables": {
"modbusValue": {
@ -48,7 +48,7 @@
"children": {},
"hidden": false,
"scriptFunction": null,
"operatingInstructions": ""
"operatingInstructions": "## 通用读取 Modbus 值(Name) (ModbusCommonReadNameBp)\n\n### 描述\n本块用于通过设备名称和地址读取Modbus设备的值。\n\n### 输入参数\n\n| 参数名 | 是否必填 | 类型 | 描述 |\n|-------------|---------|----------|------------|\n| instanceName | 必填 | String | Name |\n| address | 选填 | Integer | 地址编号 |\n| remark | 选填 | String | 地址说明 |\n\n### 上下文变量\n\n| 变量名 | 类型 | 描述 |\n|-------------|----------|------------|\n| modbusValue | String | Modbus值 |\n\n### 输出参数\n无"
},
{
"type": "Normal",

View File

@ -29,7 +29,8 @@
},
"children": {},
"hidden": false,
"scriptFunction": null
"scriptFunction": null,
"operatingInstructions": "## 校验任务实例Id是否存在 (CheckTaskRecordIdIsExistBp)\n\n### 描述\n本块用于校验任务实例Id是否存在。\n\n### 输入参数\n\n| 参数名 | 是否必填 | 类型 | 描述 |\n|--------------|---------|--------|------------|\n| taskRecordId | 必填 | String | 任务实例Id |\n\n### 上下文变量\n\n| 变量名 | 类型 | 描述 |\n|-----------------|----------|-----------|\n| taskRecordIdIsExist | boolean | Id是否存在 |\n"
},
{
"type": "All",
@ -42,7 +43,8 @@
"contextVariables": {},
"children": {},
"hidden": false,
"scriptFunction": null
"scriptFunction": null,
"operatingInstructions": "## 立即释放任务资源 (ReleaseResourceBp)\n\n### 描述\n本块用于立即释放任务资源。\n\n### 输入参数\n无\n\n### 输出参数\n无\n"
},
{
"type": "All",
@ -73,7 +75,7 @@
"extraInputParamsFunc": "",
"outputParams": {},
"contextVariables": {
"currentTimeStamp": {
"timestamp": {
"type": "Long",
"label": "当前时间戳",
"description": null
@ -82,7 +84,7 @@
"children": {},
"hidden": false,
"scriptFunction": null,
"operatingInstructions": "## 当前时间戳 (CurrentTimeStampBp)\n\n### 描述\n本块用于获取当前时间戳。\n\n### 输入参数\n无\n\n### 上下文变量\n\n| 变量名 | 类型 | 描述 |\n|-----------------|-------|------------|\n\n| currentTimeStamp | Long | 当前时间戳 |\n\n## 执行sql (JdbcExecuteBp)\n### 描述\n本块用于执行sql语句。\n\n### 输入参数\n\n| 参数名 | 是否必填 | 类型 | 描述 |\n|-------|---------|--------|---------|\n| sql | 必填 | String | sql语句 |\n\n### 输出参数\n无\n"
"operatingInstructions": "## 当前时间戳 (CurrentTimeStampBp)\n\n### 描述\n本块用于获取当前时间戳。\n\n### 输入参数\n无\n\n### 上下文变量\n\n| 变量名 | 类型 | 描述 |\n|-----------------|-------|------------|\n\n| timestamp | Long | 当前时间戳 |\n\n## 执行sql (JdbcExecuteBp)\n### 描述\n本块用于执行sql语句。\n\n### 输入参数\n\n| 参数名 | 是否必填 | 类型 | 描述 |\n|-------|---------|--------|---------|\n| sql | 必填 | String | sql语句 |\n\n### 输出参数\n无\n"
},
{
"type": "All",

View File

@ -13,7 +13,8 @@
"contextVariables": {},
"children": {},
"hidden": false,
"scriptFunction": null
"scriptFunction": null,
"operatingInstructions": "## break (BreakBp)\n\n### 描述\n本块用于中断执行。\n\n### 输入参数\n无\n\n### 输出参数\n无\n"
},
{
"type": "All",
@ -162,7 +163,8 @@
}
},
"hidden": false,
"scriptFunction": null
"scriptFunction": null,
"operatingInstructions": "## 并行执行 (ParallelFlowBp)\n\n### 描述\n本块用于并行执行。\n\n### 输入参数\n无\n\n### 输出参数\n无\n"
},
{
"type": "Normal",

View File

@ -73,9 +73,9 @@
]
},
{
"name": "JackLoad",
"name": "pick",
"type": "String",
"label": "顶升 JackLoad",
"label": "顶升 pick",
"description": "",
"required": false,
"defaultValue": null,
@ -120,9 +120,9 @@
]
},
{
"name": "JackUnload",
"name": "drop",
"type": "String",
"label": "顶降 JackUnload",
"label": "顶降 drop",
"description": "",
"required": false,
"defaultValue": null,

View File

@ -32,7 +32,8 @@
"contextVariables": {},
"children": {},
"hidden": false,
"scriptFunction": null
"scriptFunction": null,
"operatingInstructions": "## 运行脚本 (RunScript)\n\n### 描述\n本块用于运行脚本。\n\n### 输入参数\n\n| 参数名 | 是否必填 | 类型 | 描述 |\n\n|---------|---------|------|------|\n| functionName | 必填 | String | 函数名 |\n| functionArgs | 非必填 | Any | 函数参数 |\n\n### 输出参数\n无\n"
},
{
"type": "All",
@ -64,7 +65,8 @@
"contextVariables": {},
"children": {},
"hidden": false,
"scriptFunction": null
"scriptFunction": null,
"operatingInstructions": "## 脚本设置task.variables (SetScriptVariables)\n\n### 描述\n本块用于设置task.variables。\n\n### 输入参数\n\n| 参数名 | 是否必填 | 类型 | 描述 |\n\n|---------|---------|------|------|\n| functionName | 必填 | String | 函数名 |\n| functionArgs | 非必填 | Any | 函数参数 |\n\n### 输出参数\n无\n"
}
]
}

View File

@ -507,7 +507,8 @@
"contextVariables": {},
"children": {},
"hidden": false,
"scriptFunction": null
"scriptFunction": null,
"operatingInstructions": "## 设置库位为空 (SetSiteEmptyBp)\n\n### 描述\n本块用于设置库位为空。\n\n### 输入参数\n\n| 参数名 | 是否必填 | 类型 | 描述 |\n\n|---------|---------|------|------|\n| siteId | 必填 | String | 库位Id |\n\n### 输出参数\n无\n"
},
{
"type": "All",

View File

@ -1,564 +0,0 @@
{
"inputParams": [
{
"name": "input",
"type": "String",
"label": "input",
"remark": "",
"defaultValue": "",
"required": false
}
],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 1,
"name": "b1",
"blockType": "CSelectAgvBp",
"children": {
"default": [
{
"id": 2,
"name": "b2",
"blockType": "CAgvOperationBp",
"children": {},
"inputParams": {
"targetSiteLabel": {
"type": "Simple",
"value": "ko1",
"required": true
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"inputParams": {
"keyRoute": {
"type": "Simple",
"value": "apt",
"required": true
},
"group": {
"type": "Simple",
"value": "bbo"
},
"vehicle": {
"type": "Simple",
"value": "boot1"
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"selected": true,
"refTaskDefId": "",
"expanded": true
}
}
===============================
{
"inputParams": [],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 0,
"name": "bb7",
"blockType": "PrintBp",
"inputParams": {
"message": {
"type": "Simple",
"value": "123",
"required": false
}
},
"children": {},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
}
================================
{
"taskId": "ac1b5b23-f628-45a4-8053-847405406db1",
"source_type": 1,
"source_system": "SYSTEM",
"source_device": "AB2223Ndsa1",
"params": [
{
"name": "a",
"type": "整数",
"label": "a",
"defaultValue": "1"
},
{
"name": "b",
"type": "整数",
"label": "b",
"defaultValue": "1"
}
]
}
===================================
{
"inputParams": [
{
"name": "1222",
"type": "String",
"label": "231",
"remark": "12",
"defaultValue": "333",
"required": false
}
],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 1,
"name": "b1",
"blockType": "WhileBp",
"children": {},
"inputParams": {
"loopCondition": {
"type": "Simple",
"value": true,
"required": true
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"selected": false,
"refTaskDefId": "",
"expanded": true
}
}
=====================
{
"inputParams": [
{
"name": "1222",
"type": "String",
"label": "231",
"remark": "12",
"defaultValue": "333",
"required": false
}
],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 1,
"name": "b1",
"blockType": "CSelectAgvBp",
"children": {},
"inputParams": {
"priority": {
"type": "Simple",
"value": "10"
},
"vehicle": {
"type": "Simple",
"value": "amr1"
},
"group": {
"type": "Simple",
"value": "group_amr"
},
"keyRoute": {
"type": "Simple",
"value": "apt1",
"required": true
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"selected": false,
"refTaskDefId": "",
"expanded": true
}
}
=========================
{
"inputParams": [
{
"name": "1222",
"type": "String",
"label": "231",
"remark": "12",
"defaultValue": "333",
"required": false
}
],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 1,
"name": "b1",
"blockType": "CSelectAgvBp",
"children": {},
"inputParams": {
"priority": {
"type": "Simple",
"value": "10"
},
"vehicle": {
"type": "Simple",
"value": "amr1"
},
"group": {
"type": "Simple",
"value": "group_amr"
},
"keyRoute": {
"type": "Simple",
"value": "apt1",
"required": true
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"selected": false,
"refTaskDefId": "",
"expanded": true
}
}
=================================
{
"inputParams": [
{
"name": "测试",
"type": "Boolean",
"label": "132",
"remark": "",
"defaultValue": "213",
"required": false
}
],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 2,
"name": "b2",
"blockType": "CSelectAgvBp",
"children": {
"default": [
{
"id": 3,
"name": "b3",
"blockType": "CAgvOperationBp",
"children": {},
"inputParams": {
"targetSiteLabel": {
"type": "Simple",
"value": "wqlo",
"required": true
},
"scriptName": {
"type": "Simple",
"value": "JackUnload"
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"inputParams": {
"keyRoute": {
"type": "Simple",
"value": "TK01",
"required": true
},
"vehicle": {
"type": "Simple",
"value": ""
},
"priority": {
"type": "Simple",
"value": "1"
},
"tag": {
"type": "Simple",
"value": ""
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"selected": false,
"refTaskDefId": "",
"expanded": true
}
}
================================
{
"inputParams": [
{
"name": "测试",
"type": "Boolean",
"label": "132",
"remark": "",
"defaultValue": "213",
"required": false
}
],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 1,
"name": "b1",
"blockType": "CSelectAgvBp",
"inputParams": {
"keyRoute": {
"type": "Simple",
"value": "TK01",
"required": true
}
},
"children": {
"default": [
{
"id": 2,
"name": "b2",
"blockType": "CAgvOperationBp",
"children": {},
"inputParams": {
"targetSiteLabel": {
"type": "Simple",
"value": "PT02",
"required": true
},
"scriptName": {
"type": "Simple",
"value": "JackUnload"
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"selected": false,
"refTaskDefId": "",
"expanded": true
}
}
==================================
{
"inputParams": [],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 1,
"name": "b3",
"blockType": "CSelectAgvBp",
"inputParams": {
"priority": {
"type": "Simple",
"value": null,
"required": false
},
"vehicle": {
"type": "Simple",
"value": null,
"required": false
},
"group": {
"type": "Simple",
"value": null,
"required": false
},
"keyRoute": {
"type": "Simple",
"value": "TK01",
"required": true
}
},
"children": {
"default": [
{
"id": 1,
"name": "b4",
"blockType": "CAgvOperationBp",
"inputParams": {
"targetSiteLabel": {
"type": "Simple",
"value": "PT02",
"required": true
},
"spin": {
"type": "Simple",
"value": false,
"required": false
},
"task": {
"type": "Simple",
"value": "JackUnload",
"required": false
}
},
"children": {},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
}
==============================
{
"inputParams": [
{
"name": "",
"type": "String",
"label": "",
"remark": "",
"defaultValue": "",
"required": false
}
],
"outputParams": [],
"rootBlock": {
"id": -1,
"name": "-1",
"blockType": "RootBp",
"inputParams": {},
"children": {
"default": [
{
"id": 1,
"name": "b1",
"blockType": "WhileBp",
"children": {
"default": [
{
"id": 2,
"name": "b2",
"blockType": "PrintBp",
"children": {},
"inputParams": {
"message": {
"type": "Expression",
"value": "========="
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"inputParams": {
"loopCondition": {
"type": "Simple",
"value": true,
"required": true
},
"runOnce": {
"type": "Simple",
"value": null
}
},
"refTaskDefId": "",
"selected": false,
"expanded": true
}
]
},
"selected": false,
"refTaskDefId": "",
"expanded": true
}
}

View File

@ -6,6 +6,7 @@
仅包含数据库连接的配置信息
"""
from urllib.parse import quote_plus
from sqlalchemy.ext.declarative import declarative_base
from config.settings import settings
@ -18,15 +19,15 @@ class DBConfig:
# 数据库连接URL
DATABASE_URL = settings.DATABASE_URL
# 数据库连接参数
DATABASE_ARGS = settings.DATABASE_ARGS
# 数据库连接参数强制关闭echo
DATABASE_ARGS = {**settings.DATABASE_ARGS, "echo": False}
# 异步数据库连接参数
ASYNC_DATABASE_ARGS = {
"pool_size": settings.DB_POOL_SIZE,
"max_overflow": settings.DB_MAX_OVERFLOW,
"pool_recycle": settings.DB_POOL_RECYCLE,
"echo": settings.DB_ECHO,
"echo": False, # 强制关闭SQL日志输出
"pool_pre_ping": True
}
@ -47,7 +48,10 @@ class DBConfig:
# 根据数据库类型创建不同的连接URL
if settings.DB_DIALECT == 'sqlite':
return "sqlite:///:memory:"
return f"{settings.DB_DIALECT}+{settings.DB_DRIVER}://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/mysql"
# 对用户名和密码进行URL编码避免特殊字符如@符号)造成解析错误
encoded_user = quote_plus(settings.DB_USER)
encoded_password = quote_plus(settings.DB_PASSWORD)
return f"{settings.DB_DIALECT}+{settings.DB_DRIVER}://{encoded_user}:{encoded_password}@{settings.DB_HOST}:{settings.DB_PORT}/mysql"
# 缓存配置

View File

@ -0,0 +1,631 @@
# -*- coding: utf-8 -*-
"""
错误代码映射配置
用于为不同类型的错误分配固定的告警代码
"""
from typing import Dict, List, Tuple
import re
class ErrorCodeMapping:
"""错误代码映射管理器"""
# 基础告警代码范围定义
BASE_RANGES = {
'system': (5400, 5499), # 系统相关错误 5400-5499
'task': (5500, 5699), # 任务相关错误 5500-5699
'robot': (5700, 5799), # 机器人相关错误 5700-5799
'database': (5800, 5849), # 数据库相关错误 5800-5849
'network': (5850, 5899), # 网络相关错误 5850-5899
'auth': (5900, 5949), # 认证相关错误 5900-5949
'validation': (5950, 5999), # 验证相关错误 5950-5999
}
# 错误类型和关键词映射
ERROR_TYPE_PATTERNS = {
# 任务相关错误
'task_execution_failed': {
'patterns': [
r'任务执行失败',
r'task execution failed',
r'execute.*failed',
r'执行.*失败',
],
'code': 5500,
'category': 'task'
},
'task_execution_timeout': {
'patterns': [
r'任务执行超时',
r'task execution timeout',
r'execute.*timeout',
r'执行.*超时',
],
'code': 5501,
'category': 'task'
},
'task_scheduling_failed': {
'patterns': [
r'任务调度失败',
r'task scheduling failed',
r'调度.*失败',
r'scheduling.*failed',
],
'code': 5502,
'category': 'task'
},
'task_not_found': {
'patterns': [
r'任务.*不存在',
r'task.*not found',
r'找不到.*任务',
r'任务.*未找到',
],
'code': 5503,
'category': 'task'
},
'task_params_invalid': {
'patterns': [
r'任务参数.*无效',
r'task parameters invalid',
r'参数.*错误',
r'invalid.*parameters',
],
'code': 5504,
'category': 'task'
},
'task_status_error': {
'patterns': [
r'任务状态.*错误',
r'task status error',
r'状态.*异常',
r'status.*error',
],
'code': 5505,
'category': 'task'
},
'task_block_execution_failed': {
'patterns': [
r'块.*执行.*失败',
r'block.*execution.*failed',
r'任务块.*失败',
r'block.*failed',
],
'code': 5506,
'category': 'task'
},
'task_template_error': {
'patterns': [
r'任务模板.*错误',
r'task template error',
r'模板.*异常',
r'template.*error',
],
'code': 5507,
'category': 'task'
},
'task_sync_failed': {
'patterns': [
r'同步任务.*失败',
r'sync.*task.*failed',
r'同步任务到.*系统.*失败',
r'任务同步.*失败',
],
'code': 5508,
'category': 'task'
},
'task_start_failed': {
'patterns': [
r'启动任务失败',
r'start.*task.*failed',
r'任务启动.*失败',
r'task.*startup.*failed',
],
'code': 5509,
'category': 'task'
},
'task_run_failed': {
'patterns': [
r'运行任务失败',
r'run.*task.*failed',
r'任务运行.*失败',
r'task.*execution.*failed',
],
'code': 5510,
'category': 'task'
},
'task_scene_id_not_exist': {
'patterns': [
r'此场景id不存在',
r'场景.*不存在',
r'scene.*id.*not.*exist',
r'scene.*not.*found',
],
'code': 5511,
'category': 'task'
},
'task_child_block_failed': {
'patterns': [
r'子块.*执行失败',
r'child.*block.*failed',
r'子块.*失败',
r'child.*execution.*failed',
],
'code': 5512,
'category': 'task'
},
# 机器人相关错误
'robot_connection_failed': {
'patterns': [
r'机器人.*连接.*失败',
r'robot.*connection.*failed',
r'AGV.*连接.*失败',
r'vehicle.*connection.*failed',
],
'code': 5700,
'category': 'robot'
},
'robot_battery_low': {
'patterns': [
r'机器人.*电池.*低',
r'robot.*battery.*low',
r'AGV.*电量.*低',
r'电池.*不足',
],
'code': 5701,
'category': 'robot'
},
'robot_navigation_failed': {
'patterns': [
r'机器人.*导航.*失败',
r'robot.*navigation.*failed',
r'AGV.*导航.*失败',
r'导航.*异常',
],
'code': 5702,
'category': 'robot'
},
'robot_task_failed': {
'patterns': [
r'机器人.*任务.*失败',
r'robot.*task.*failed',
r'AGV.*任务.*失败',
r'机器人.*执行.*失败',
],
'code': 5703,
'category': 'robot'
},
'robot_select_amr_failed': {
'patterns': [
r'为任务选择AMR失败',
r'选择.*AMR.*失败',
r'select.*amr.*failed',
r'amr.*selection.*failed',
],
'code': 5704,
'category': 'robot'
},
'robot_select_execution_failed': {
'patterns': [
r'选择执行机器人失败',
r'选择.*机器人.*失败',
r'robot.*selection.*failed',
r'select.*execution.*robot.*failed',
],
'code': 5705,
'category': 'robot'
},
'robot_tianfeng_task_id_not_exist': {
'patterns': [
r'此天风任务id不存在',
r'天风任务.*不存在',
r'tianfeng.*task.*id.*not.*exist',
r'tianfeng.*task.*not.*found',
],
'code': 5706,
'category': 'robot'
},
'robot_block_action_detail_failed': {
'patterns': [
r'调用获取任务块动作详情接口失败',
r'获取任务块动作.*详情.*失败',
r'task.*block.*action.*detail.*failed',
r'get.*task.*block.*action.*failed',
],
'code': 5707,
'category': 'robot'
},
'robot_block_detail_failed': {
'patterns': [
r'调用获取任务块详情接口失败',
r'获取任务块.*详情.*失败',
r'task.*block.*detail.*failed',
r'get.*task.*block.*failed',
],
'code': 5708,
'category': 'robot'
},
# 数据库相关错误
'database_connection_failed': {
'patterns': [
r'数据库.*连接.*失败',
r'database.*connection.*failed',
r'DB.*连接.*失败',
r'连接.*数据库.*失败',
],
'code': 5800,
'category': 'database'
},
'database_query_failed': {
'patterns': [
r'数据库.*查询.*失败',
r'database.*query.*failed',
r'SQL.*执行.*失败',
r'查询.*失败',
],
'code': 5801,
'category': 'database'
},
'database_transaction_failed': {
'patterns': [
r'数据库.*事务.*失败',
r'database.*transaction.*failed',
r'事务.*回滚',
r'transaction.*rollback',
],
'code': 5802,
'category': 'database'
},
# 网络相关错误
'network_connection_failed': {
'patterns': [
r'网络.*连接.*失败',
r'network.*connection.*failed',
r'连接.*超时',
r'connection.*timeout',
],
'code': 5850,
'category': 'network'
},
'network_request_failed': {
'patterns': [
r'网络.*请求.*失败',
r'network.*request.*failed',
r'HTTP.*请求.*失败',
r'request.*failed',
],
'code': 5851,
'category': 'network'
},
'network_timeout': {
'patterns': [
r'网络.*超时',
r'network.*timeout',
r'请求.*超时',
r'request.*timeout',
r'TimeoutError',
r'CancelledError',
],
'code': 5852,
'category': 'network'
},
'network_connect_refused': {
'patterns': [
r'ConnectionRefusedError',
r'Connect call failed',
r'Cannot connect to host',
r'连接.*被拒绝',
r'connection.*refused',
],
'code': 5853,
'category': 'network'
},
'network_api_call_failed': {
'patterns': [
r'调用.*接口失败',
r'调用.*API.*失败',
r'api.*call.*failed',
r'interface.*call.*failed',
r'调用系统任务创建任务接口失败',
],
'code': 5854,
'category': 'network'
},
# 系统相关错误
'system_resource_exhausted': {
'patterns': [
r'系统.*资源.*耗尽',
r'system.*resource.*exhausted',
r'内存.*不足',
r'memory.*insufficient',
],
'code': 5400,
'category': 'system'
},
'system_configuration_error': {
'patterns': [
r'系统.*配置.*错误',
r'system.*configuration.*error',
r'配置.*异常',
r'configuration.*error',
],
'code': 5401,
'category': 'system'
},
'system_service_unavailable': {
'patterns': [
r'系统.*服务.*不可用',
r'system.*service.*unavailable',
r'服务.*不可用',
r'service.*unavailable',
],
'code': 5402,
'category': 'system'
},
# 认证相关错误
'auth_token_invalid': {
'patterns': [
r'认证.*令牌.*无效',
r'auth.*token.*invalid',
r'token.*过期',
r'token.*expired',
],
'code': 5900,
'category': 'auth'
},
'auth_permission_denied': {
'patterns': [
r'权限.*拒绝',
r'permission.*denied',
r'访问.*被拒绝',
r'access.*denied',
],
'code': 5901,
'category': 'auth'
},
# 验证相关错误
'validation_parameter_invalid': {
'patterns': [
r'参数.*验证.*失败',
r'parameter.*validation.*failed',
r'参数.*无效',
r'invalid.*parameter',
],
'code': 5950,
'category': 'validation'
},
'validation_data_format_error': {
'patterns': [
r'数据.*格式.*错误',
r'data.*format.*error',
r'格式.*不正确',
r'format.*invalid',
],
'code': 5951,
'category': 'validation'
},
# 程序错误相关
'program_attribute_error': {
'patterns': [
r'AttributeError',
r'\'.*\' object has no attribute \'.*\'',
r'属性.*错误',
r'attribute.*error',
r'\'NoneType\' object has no attribute \'get\'',
],
'code': 5952,
'category': 'validation'
},
'program_type_error': {
'patterns': [
r'TypeError',
r'类型.*错误',
r'type.*error',
r'NoneType.*object',
],
'code': 5953,
'category': 'validation'
},
'program_runtime_error': {
'patterns': [
r'RuntimeError',
r'运行时.*错误',
r'runtime.*error',
r'执行时.*错误',
],
'code': 5954,
'category': 'validation'
},
}
# 模块特定的错误代码偏移
MODULE_OFFSETS = {
'services.task_service': 0,
'services.task_execution': 5,
'services.task_scheduler': 10,
'services.enhanced_scheduler.task_scheduler': 15,
'services.task_edit_service': 20,
'services.sync_service': 25,
'services.robot_scheduler': 30,
'services.calldevice_service': 35,
'services.modbus_config_service': 40,
'services.script_service': 45,
'services.execution.block_executor': 50,
'services.execution.task_executor': 55,
'routes.task_api': 60,
'routes.task_edit_api': 65,
'routes.calldevice_api': 70,
'routes.modbus_config_api': 75,
'app.task_edit_api': 80,
'middlewares.error_handlers': 85,
'middleware.request_logger': 90,
'utils.alert_sync': 95,
'data.models': 100,
'data.session': 105,
'core.intelligence': 110,
}
@classmethod
def get_error_code(cls, logger_name: str, message: str) -> int:
"""
根据logger名称和消息内容获取错误代码
Args:
logger_name: logger名称
message: 错误消息
Returns:
错误代码
"""
# 首先尝试匹配错误类型
error_type = cls._match_error_type(message)
if error_type:
# 获取基础代码
base_code = cls.ERROR_TYPE_PATTERNS[error_type]['code']
# 根据模块添加偏移量
module_offset = cls._get_module_offset(logger_name)
# 确保代码在合理范围内
final_code = base_code + module_offset
# 确保不超过最大值
if final_code > 9999:
final_code = base_code
return final_code
# 如果没有匹配的错误类型,使用通用代码生成
return cls._generate_fallback_code(logger_name, message)
@classmethod
def _match_error_type(cls, message: str) -> str:
"""
匹配错误类型
Args:
message: 错误消息
Returns:
错误类型名称如果没有匹配则返回None
"""
message_lower = message.lower()
for error_type, config in cls.ERROR_TYPE_PATTERNS.items():
for pattern in config['patterns']:
if re.search(pattern, message_lower, re.IGNORECASE):
return error_type
return None
@classmethod
def _get_module_offset(cls, logger_name: str) -> int:
"""
获取模块的偏移量
Args:
logger_name: logger名称
Returns:
偏移量
"""
# 尝试精确匹配
if logger_name in cls.MODULE_OFFSETS:
return cls.MODULE_OFFSETS[logger_name]
# 尝试部分匹配
for module, offset in cls.MODULE_OFFSETS.items():
if logger_name.startswith(module):
return offset
return 0
@classmethod
def _generate_fallback_code(cls, logger_name: str, message: str) -> int:
"""
生成备用错误代码用于无法匹配的错误
Args:
logger_name: logger名称
message: 错误消息
Returns:
错误代码
"""
import hashlib
# 使用简化的消息内容生成代码
simplified_message = cls._simplify_message(message)
hash_input = f"{logger_name}:{simplified_message}"
hash_value = hashlib.md5(hash_input.encode()).hexdigest()
# 生成在6000-6999范围内的代码未分类错误
code = int(hash_value[:4], 16) % 1000 + 6000
return code
@classmethod
def _simplify_message(cls, message: str) -> str:
"""
简化错误消息去除变量部分
Args:
message: 原始错误消息
Returns:
简化后的消息
"""
# 移除常见的变量部分
simplified = re.sub(r'\d+', '[NUMBER]', message) # 替换数字
simplified = re.sub(r'[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}', '[UUID]', simplified) # 替换UUID
simplified = re.sub(r'/[^/\s]+', '[PATH]', simplified) # 替换路径
simplified = re.sub(r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}', '[IP]', simplified) # 替换IP地址
simplified = re.sub(r'\s+', ' ', simplified).strip() # 清理空白字符
return simplified
@classmethod
def get_error_info(cls, error_code: int) -> Dict:
"""
根据错误代码获取错误信息
Args:
error_code: 错误代码
Returns:
错误信息字典
"""
# 查找匹配的错误类型
for error_type, config in cls.ERROR_TYPE_PATTERNS.items():
if config['code'] <= error_code < config['code'] + 200: # 允许偏移量
return {
'error_type': error_type,
'category': config['category'],
'base_code': config['code'],
'description': f"{config['category']}相关错误: {error_type}"
}
# 根据代码范围确定类别
for category, (start, end) in cls.BASE_RANGES.items():
if start <= error_code <= end:
return {
'error_type': 'unknown',
'category': category,
'base_code': start,
'description': f"{category}相关错误"
}
return {
'error_type': 'unknown',
'category': 'unknown',
'base_code': error_code,
'description': '未知错误'
}

View File

@ -8,6 +8,7 @@
import os
from typing import Dict, Any, Optional, List
from urllib.parse import quote_plus
from pydantic import Field
from pydantic_settings import BaseSettings
@ -25,23 +26,36 @@ default_server_config = {
'log_level': 'info'
}
# 生产环境服务器配置
production_server_config = {
'host': '0.0.0.0',
'port': 8000,
'workers': 4,
'reload': False,
'log_level': 'warning'
'workers': 1, # 推荐根据CPU核心数调整: (CPU核心数 × 2) + 1
'reload': False, # 生产环境必须关闭热重载
'log_level': 'warning' # 生产环境减少日志输出
}
# 测试环境服务器配置
test_server_config = {
'host': '127.0.0.1',
'port': 8001,
'workers': 1,
'workers': 2, # 测试环境适中配置
'reload': False,
'log_level': 'debug'
'log_level': 'info'
}
# 数据库配置
# default_db_config = {
# 'dialect': 'mysql',
# 'driver': 'pymysql',
# 'username': 'remote_admin',
# 'password': 'aiit@0619',
# 'host': '192.168.189.82',
# 'port': 3306,
# 'database': 'vwed_task',
# 'charset': 'utf8mb4'
# }
default_db_config = {
'dialect': 'mysql',
'driver': 'pymysql',
@ -52,7 +66,6 @@ default_db_config = {
'database': 'vwed_task',
'charset': 'utf8mb4'
}
# Redis配置
default_redis_config = {
'host': 'localhost',
@ -65,48 +78,48 @@ default_redis_config = {
'decode_responses': True
}
test_redis_config = {
'host': 'localhost',
'port': 6379,
'db': 1,
'password': None,
'prefix': 'vwed_test:',
'decode_responses': True
}
# test_redis_config = {
# 'host': 'localhost',
# 'port': 6379,
# 'db': 1,
# 'password': None,
# 'prefix': 'vwed_test:',
# 'decode_responses': True
# }
# 库位服务API端点映射
storage_api_endpoints = {
"batch_setting_site": "/site/batch-setting",
"get_idle_crowded_site": "/site/idle-crowded",
"get_idle_site": "/site/idle",
"get_locked_sites_by_task_record_id": "/site/locked-by-task",
"get_site_attr": "/site/attr",
"query_idle_site": "/site/query",
"set_site_attr": "/site/attr",
"set_site_content": "/site/content",
"set_site_empty": "/site/empty",
"set_site_filled": "/site/filled",
"set_site_locked": "/site/lock",
"set_site_tags": "/site/tags",
"set_site_unlocked": "/site/unlock"
}
# # 库位服务API端点映射
# storage_api_endpoints = {
# "batch_setting_site": "/site/batch-setting",
# "get_idle_crowded_site": "/site/idle-crowded",
# "get_idle_site": "/site/idle",
# "get_locked_sites_by_task_record_id": "/site/locked-by-task",
# "get_site_attr": "/site/attr",
# "query_idle_site": "/site/query",
# "set_site_attr": "/site/attr",
# "set_site_content": "/site/content",
# "set_site_empty": "/site/empty",
# "set_site_filled": "/site/filled",
# "set_site_locked": "/site/lock",
# "set_site_tags": "/site/tags",
# "set_site_unlocked": "/site/unlock"
# }
# 库位服务API HTTP方法映射
storage_api_methods = {
"batch_setting_site": "POST",
"get_idle_crowded_site": "GET",
"get_idle_site": "GET",
"get_locked_sites_by_task_record_id": "GET",
"get_site_attr": "GET",
"query_idle_site": "GET",
"set_site_attr": "PUT",
"set_site_content": "PUT",
"set_site_empty": "PUT",
"set_site_filled": "PUT",
"set_site_locked": "PUT",
"set_site_tags": "PUT",
"set_site_unlocked": "PUT"
}
# # 库位服务API HTTP方法映射
# storage_api_methods = {
# "batch_setting_site": "POST",
# "get_idle_crowded_site": "GET",
# "get_idle_site": "GET",
# "get_locked_sites_by_task_record_id": "GET",
# "get_site_attr": "GET",
# "query_idle_site": "GET",
# "set_site_attr": "PUT",
# "set_site_content": "PUT",
# "set_site_empty": "PUT",
# "set_site_filled": "PUT",
# "set_site_locked": "PUT",
# "set_site_tags": "PUT",
# "set_site_unlocked": "PUT"
# }
# 机器人调度服务API端点映射
robot_api_endpoints = {
@ -122,17 +135,54 @@ robot_api_methods = {
"get_pgv_code": "GET"
}
# 呼叫器设备服务API端点映射
calldevice_api_endpoints = {
"get_device_state": "/jeecg-boot/device/getDeviceState",
"init_device": "/jeecg-boot/device/initDevice",
"set_device_state": "/jeecg-boot/device/setDeviceState"
}
# 呼叫器设备服务API HTTP方法映射
calldevice_api_methods = {
"get_device_state": "POST",
"init_device": "POST",
"set_device_state": "POST"
}
# 任务执行API端点映射
task_execution_api_endpoints = {
"run_task": "/api/vwed-task-edit/run",
"stop_task": "/api/vwed-task-record/stop"
}
# 任务执行API HTTP方法映射
task_execution_api_methods = {
"run_task": "POST",
"stop_task": "POST"
}
# 外部服务API配置
external_api_config = {
"storage": {
"base_url": "http://localhost:8080/api/storage",
"endpoints": storage_api_endpoints,
"methods": storage_api_methods
},
# "storage": {
# "base_url": "http://localhost:8080/api/storage",
# "endpoints": storage_api_endpoints,
# "methods": storage_api_methods
# },
"robot": {
"base_url": "http://localhost:8080/api/robot",
"endpoints": robot_api_endpoints,
"methods": robot_api_methods
},
"calldevice": {
"base_url": "http://82.156.39.91:18080",
"init_base_url": "http://82.156.39.91:18080",
"endpoints": calldevice_api_endpoints,
"methods": calldevice_api_methods
},
"task_execution": {
"base_url": "http://127.0.0.1:8000",
"endpoints": task_execution_api_endpoints,
"methods": task_execution_api_methods
}
}
@ -150,8 +200,8 @@ def get_config_for_env(env: str, config_type: str) -> Dict[str, Any]:
return default_db_config
elif config_type == 'redis':
if env == 'test':
return test_redis_config
# if env == 'test':
# return test_redis_config
return default_redis_config
return {}
@ -169,8 +219,7 @@ class BaseConfig(BaseSettings):
API_PREFIX: str = "/api"
# 获取当前环境
_env: str = os.getenv("APP_ENV", "development").lower()
_env: str = os.getenv("APP_ENV", "production").lower()
# 服务配置
_server_config = get_config_for_env(_env, 'server')
SERVER_HOST: str = Field(default=_server_config['host'], env="HOST")
@ -221,12 +270,12 @@ class BaseConfig(BaseSettings):
API_MOCK_MODE: bool = Field(default=False, env="API_MOCK_MODE") # 是否启用API模拟模式
# 库位服务API配置
STORAGE_API_BASE_URL: str = Field(default=external_api_config["storage"]["base_url"], env="STORAGE_API_BASE_URL")
STORAGE_API_ENDPOINTS: Dict[str, str] = external_api_config["storage"]["endpoints"]
STORAGE_API_METHODS: Dict[str, str] = external_api_config["storage"]["methods"]
STORAGE_API_TIMEOUT: int = Field(default=30, env="STORAGE_API_TIMEOUT")
STORAGE_API_TOKEN: Optional[str] = Field(default=None, env="STORAGE_API_TOKEN")
STORAGE_API_MOCK_MODE: bool = Field(default=False, env="STORAGE_API_MOCK_MODE")
# STORAGE_API_BASE_URL: str = Field(default=external_api_config["storage"]["base_url"], env="STORAGE_API_BASE_URL")
# STORAGE_API_ENDPOINTS: Dict[str, str] = external_api_config["storage"]["endpoints"]
# STORAGE_API_METHODS: Dict[str, str] = external_api_config["storage"]["methods"]
# STORAGE_API_TIMEOUT: int = Field(default=30, env="STORAGE_API_TIMEOUT")
# STORAGE_API_TOKEN: Optional[str] = Field(default=None, env="STORAGE_API_TOKEN")
# STORAGE_API_MOCK_MODE: bool = Field(default=False, env="STORAGE_API_MOCK_MODE")
# 机器人调度服务API配置
ROBOT_API_BASE_URL: str = Field(default=external_api_config["robot"]["base_url"], env="ROBOT_API_BASE_URL")
@ -236,6 +285,23 @@ class BaseConfig(BaseSettings):
ROBOT_API_TOKEN: Optional[str] = Field(default=None, env="ROBOT_API_TOKEN")
ROBOT_API_MOCK_MODE: bool = Field(default=False, env="ROBOT_API_MOCK_MODE")
# 呼叫器设备服务API配置
CALLDEVICE_API_BASE_URL: str = Field(default=external_api_config["calldevice"]["base_url"], env="CALLDEVICE_API_BASE_URL")
CALLDEVICE_API_INIT_BASE_URL: str = Field(default=external_api_config["calldevice"]["init_base_url"], env="CALLDEVICE_API_INIT_BASE_URL")
CALLDEVICE_API_ENDPOINTS: Dict[str, str] = external_api_config["calldevice"]["endpoints"]
CALLDEVICE_API_METHODS: Dict[str, str] = external_api_config["calldevice"]["methods"]
CALLDEVICE_API_TIMEOUT: int = Field(default=10, env="CALLDEVICE_API_TIMEOUT") # 获取设备状态的超时时间
CALLDEVICE_API_INIT_TIMEOUT: int = Field(default=30, env="CALLDEVICE_API_INIT_TIMEOUT") # 初始化设备的超时时间
CALLDEVICE_API_RESET_TIMEOUT: int = Field(default=30, env="CALLDEVICE_API_RESET_TIMEOUT") # 复位按钮的超时时间
CALLDEVICE_API_TOKEN: Optional[str] = Field(default=None, env="CALLDEVICE_API_TOKEN")
CALLDEVICE_API_MOCK_MODE: bool = Field(default=False, env="CALLDEVICE_API_MOCK_MODE")
# 任务执行API配置
TASK_EXECUTION_API_BASE_URL: str = Field(default=external_api_config["task_execution"]["base_url"], env="TASK_EXECUTION_API_BASE_URL")
TASK_EXECUTION_API_ENDPOINTS: Dict[str, str] = external_api_config["task_execution"]["endpoints"]
TASK_EXECUTION_API_METHODS: Dict[str, str] = external_api_config["task_execution"]["methods"]
TASK_EXECUTION_API_TIMEOUT: int = Field(default=30, env="TASK_EXECUTION_API_TIMEOUT")
# CORS设置
CORS_ORIGINS: List[str] = ["*"]
CORS_ALLOW_CREDENTIALS: bool = True
@ -277,12 +343,37 @@ class BaseConfig(BaseSettings):
TASK_SCHEDULER_AUTO_SCALE_INTERVAL: int = 120 # 自动扩缩容间隔(秒)
TASK_SCHEDULER_WORKER_HEARTBEAT_INTERVAL: int = 120 # 心跳间隔(秒)
# 告警同步配置
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_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_BATCH_SIZE: int = Field(default=10, env="ALERT_SYNC_BATCH_SIZE") # 批量发送大小
ALERT_SYNC_QUEUE_SIZE: int = Field(default=1000, env="ALERT_SYNC_QUEUE_SIZE") # 队列大小
ALERT_SYNC_MIN_LEVEL: str = Field(default="WARNING", env="ALERT_SYNC_MIN_LEVEL") # 最小告警级别
# 地图数据库区容量配置
# 密集库区容量配置
MAP_DENSE_STORAGE_BASE_CAPACITY: int = Field(default=50, env="MAP_DENSE_STORAGE_BASE_CAPACITY") # 密集库区基础容量
MAP_DENSE_STORAGE_CAPACITY_PER_POINT: int = Field(default=10, env="MAP_DENSE_STORAGE_CAPACITY_PER_POINT") # 密集库区每个动作点增加的容量
MAP_DENSE_STORAGE_LAYER_MULTIPLIER: float = Field(default=1.5, env="MAP_DENSE_STORAGE_LAYER_MULTIPLIER") # 密集库区分层倍数
# 一般库区容量配置
MAP_GENERAL_STORAGE_BASE_CAPACITY: int = Field(default=30, env="MAP_GENERAL_STORAGE_BASE_CAPACITY") # 一般库区基础容量
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") # 一般库区分层倍数
@property
def DATABASE_URL(self) -> str:
"""构建数据库连接URL"""
if self.DB_DIALECT == 'sqlite':
return f"sqlite:///{self.DB_NAME}"
return f"{self.DB_DIALECT}+{self.DB_DRIVER}://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}?charset={self.DB_CHARSET}"
# 对用户名和密码进行URL编码避免特殊字符如@符号)造成解析错误
encoded_user = quote_plus(self.DB_USER)
encoded_password = quote_plus(self.DB_PASSWORD)
return f"{self.DB_DIALECT}+{self.DB_DRIVER}://{encoded_user}:{encoded_password}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}?charset={self.DB_CHARSET}"
@property
def DATABASE_ARGS(self) -> Dict[str, Any]:
@ -300,8 +391,15 @@ class BaseConfig(BaseSettings):
def REDIS_URL(self) -> str:
"""构建Redis连接URL"""
if self.REDIS_PASSWORD:
return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
# 对Redis密码进行URL编码避免特殊字符如@符号)造成解析错误
encoded_password = quote_plus(self.REDIS_PASSWORD)
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}"
@property
def ALERT_SYNC_URL(self) -> str:
"""构建告警同步URL"""
return f"http://{self.ALERT_SYNC_HOST}:{self.ALERT_SYNC_PORT}{self.ALERT_SYNC_API_PATH}"
# 更新为Pydantic v2的配置方式
model_config = {
@ -316,9 +414,15 @@ class DevelopmentConfig(BaseConfig):
DEBUG: bool = True
DB_ECHO: bool = True # 开发环境输出SQL语句
LOG_LEVEL: str = "DEBUG"
SERVER_RELOAD: bool = True # 开发环境启用热重载
SERVER_RELOAD: bool = BaseConfig().SERVER_RELOAD
STORAGE_API_MOCK_MODE: bool = True # 开发环境默认使用API模拟模式
ROBOT_API_MOCK_MODE: bool = True # 开发环境默认使用机器人API模拟模式
# 开发环境可以使用较小的容量配置便于测试
MAP_DENSE_STORAGE_BASE_CAPACITY: int = 20
MAP_DENSE_STORAGE_CAPACITY_PER_POINT: int = 5
MAP_GENERAL_STORAGE_BASE_CAPACITY: int = 15
MAP_GENERAL_STORAGE_CAPACITY_PER_POINT: int = 8
# 根据环境变量选择配置

View File

@ -38,7 +38,8 @@ tf_api_methods = {
}
# 从环境变量读取配置,或使用默认值
TF_API_BASE_URL = os.getenv("TF_API_BASE_URL", "http://192.168.189.101:8080/jeecg-boot")
# TF_API_BASE_URL = os.getenv("TF_API_BASE_URL", "http://192.168.189.80:8080/jeecg-boot")
TF_API_BASE_URL = os.getenv("TF_API_BASE_URL", "http://111.231.146.230:4080/jeecg-boot")
TF_API_TIMEOUT = int(os.getenv("TF_API_TIMEOUT", "60"))
TF_API_RETRY_TIMES = int(os.getenv("TF_API_RETRY_TIMES", "3"))
TF_API_MOCK_MODE = False

View File

@ -5,6 +5,4 @@
VWED任务系统数据模块
"""
# 确保子模块正确加载
import data.models

Binary file not shown.

View File

@ -0,0 +1,21 @@
from enum import IntEnum
class CallDeviceStatus(IntEnum):
"""呼叫器设备状态枚举"""
START = 1
END = 0
class CallDeviceButtonStatus(IntEnum):
"""呼叫器按钮状态枚举"""
INIT: str = "0" # 初始化
DOWN: str = "1" # 按下
CANCEL: str = "0" # 释放
class CallDeviceButtonType(IntEnum):
"""呼叫器按钮信号类型枚举"""
BUTTON = 1 # 按钮
LIGHT = 2 # 灯

View File

@ -0,0 +1,6 @@
from enum import IntEnum
class ModbusConfigStatus(IntEnum):
"""呼叫器设备状态枚举"""
START = 1
END = 0

View File

@ -6,5 +6,5 @@ class TaskBlockRecordStatus(IntEnum):
RUNNING = 1001 # 执行中
NOT_EXECUTED = 1006 # 未执行
FAILED = 2000 # 执行失败
CANCELED = 2002 # 取消
CANCELED = 2001 # 取消

View File

@ -17,11 +17,19 @@ from data.models.datacachesplit import VWEDDataCacheSplit
from data.models.script import VWEDScript, VWEDScriptVersion, VWEDScriptLog
from data.models.modbusconfig import VWEDModbusConfig
from data.models.calldevice import VWEDCallDevice, VWEDCallDeviceButton
from data.models.interfacedef import InterfaceDefHistory
from data.models.storage_area import StorageArea, StorageAreaType
from data.models.operate_point import OperatePoint
from data.models.operate_point_layer import OperatePointLayer
from data.models.extended_property import ExtendedProperty, ExtendedPropertyTypeEnum
from data.models.storage_location_log import StorageLocationLog
# 导出所有模型供应用程序使用
__all__ = [
'BaseModel', 'VWEDTaskDef', 'VWEDTaskRecord', 'VWEDTaskLog',
'VWEDBlockRecord', 'VWEDTaskTemplate', 'VWEDDataCache', 'VWEDDataCacheSplit',
'VWEDScript', 'VWEDScriptVersion', 'VWEDScriptLog', 'VWEDModbusConfig',
'VWEDCallDevice', 'VWEDCallDeviceButton'
'VWEDCallDevice', 'VWEDCallDeviceButton', 'InterfaceDefHistory',
'StorageArea', 'StorageAreaType', 'OperatePoint', 'OperatePointLayer',
'ExtendedProperty', 'ExtendedPropertyTypeEnum', 'StorageLocationLog'
]

Binary file not shown.

Binary file not shown.

View File

@ -41,7 +41,10 @@ class VWEDCallDevice(BaseModel):
ip = Column(String(50), nullable=False, comment='IP地址')
port = Column(Integer, nullable=False, comment='端口号')
device_name = Column(String(100), nullable=False, comment='设备名称')
status = Column(Integer, default=0, comment='状态(0:禁用,1:启用)')
is_init = Column(Boolean, default=False, comment='是否初始化')
status = Column(Integer, default=0, comment='状态(0:禁用,1:启用,2:初始化中)')
slave_id = Column(String(255), comment='从机ID')
# 定义关联关系
buttons = relationship("VWEDCallDeviceButton", back_populates="call_device", cascade="all, delete-orphan")
@ -64,8 +67,7 @@ class VWEDCallDeviceButton(BaseModel):
__table_args__ = (
Index('idx_vwed_calldevice_button_device_id', 'device_id'),
Index('idx_vwed_calldevice_button_button_address', 'button_address'),
Index('idx_vwed_calldevice_button_light_address', 'light_address'),
Index('idx_vwed_calldevice_button_register_address', 'register_address'),
{
'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8mb4',
@ -75,15 +77,18 @@ class VWEDCallDeviceButton(BaseModel):
)
id = Column(String(255), primary_key=True, nullable=False, comment='按钮唯一标识')
signal_name = Column(String(100), nullable=False, comment='信号名称')
signal_type = Column(Integer, nullable=False, comment='信号类型 1:按钮 2:灯')
signal_length = Column(String(100), nullable=False, comment='信号长度')
register_address = Column(String(100), nullable=False, comment='寄存器地址')
function_code = Column(String(255), comment='功能码')
device_id = Column(String(255), ForeignKey('vwed_calldevice.id'), nullable=False, comment='所属呼叫器设备ID')
button_address = Column(String(100), nullable=False, comment='按钮地址')
remark = Column(String(255), comment='备注')
light_address = Column(String(100), comment='灯地址')
press_task_id = Column(String(255), comment='按下灯亮绑定的VWED任务ID')
long_press_task_id = Column(String(255), comment='长按取消后触发VWED任务ID')
vwed_task_id = Column(String(255), comment='按下灯亮绑定的VWED任务ID')
long_vwed_task_id = Column(String(255), comment='长按取消后触发VWED任务ID')
# 定义关联关系
call_device = relationship("VWEDCallDevice", back_populates="buttons")
def __repr__(self):
return f"<VWEDCallDeviceButton(id='{self.id}', device_id='{self.device_id}', button_address='{self.button_address}', remark='{self.remark}', light_address='{self.light_address}')>"
return f"<VWEDCallDeviceButton(id='{self.id}', device_id='{self.device_id}', signal_name='{self.signal_name}', register_address='{self.register_address}', remark='{self.remark}')>"

View File

@ -0,0 +1,62 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
扩展属性数据模型
用于存储动作点的扩展属性定义
"""
from sqlalchemy import Column, String, Text, Boolean, Integer, Enum
from sqlalchemy.dialects.mysql import VARCHAR
from data.models.base import BaseModel
from enum import Enum as PyEnum
class ExtendedPropertyTypeEnum(PyEnum):
"""扩展属性类型枚举"""
STRING = "string" # 字符串
INTEGER = "integer" # 整数
FLOAT = "float" # 浮点数
BOOLEAN = "boolean" # 布尔值
DATE = "date" # 日期
DATETIME = "datetime" # 日期时间
TEXT = "text" # 长文本
SELECT = "select" # 下拉选择
MULTISELECT = "multiselect" # 多选
class ExtendedProperty(BaseModel):
"""扩展属性模型"""
__tablename__ = "extended_properties"
# 基本信息
property_key = Column(VARCHAR(200),nullable=False, index=True, comment="属性键(与属性名称相同)")
property_name = Column(VARCHAR(200), nullable=False, index=True, comment="属性名称(唯一标识)")
property_type = Column(Enum(ExtendedPropertyTypeEnum), nullable=False, comment="属性类型")
# 属性设置
is_required = Column(Boolean, default=False, comment="是否必填")
is_enabled = Column(Boolean, default=True, comment="是否启用")
# 描述信息
description = Column(Text, comment="属性描述")
placeholder = Column(VARCHAR(500), comment="输入提示")
# 默认值和选项
default_value = Column(Text, comment="默认值")
options = Column(Text, comment="选项值JSON格式用于select和multiselect类型")
# 验证规则
validation_rules = Column(Text, comment="验证规则JSON格式")
# 分类和排序
category = Column(VARCHAR(100), comment="属性分类")
sort_order = Column(Integer, default=0, comment="排序顺序")
# 显示设置
display_width = Column(Integer, default=200, comment="显示宽度(像素)")
display_format = Column(VARCHAR(100), comment="显示格式")
def __repr__(self):
return f"<ExtendedProperty(property_key='{self.property_key}', property_name='{self.property_name}', property_type='{self.property_type}')>"

View File

@ -18,7 +18,7 @@ class InterfaceDefHistory(BaseModel):
对应interfacedefhistory表
功能存储系统接口的定义和版本历史
"""
__tablename__ = 'interfacedefhistory'
__tablename__ = 'vwed_interfacedefhistory'
# 唯一约束和索引
__table_args__ = (
@ -26,7 +26,6 @@ class InterfaceDefHistory(BaseModel):
Index('idx_interfacedefhistory_method', 'method'),
Index('idx_interfacedefhistory_url', 'url'),
Index('idx_interfacedefhistory_version', 'version'),
Index('idx_interfacedefhistory_project_id', 'project_id'),
Index('idx_interfacedefhistory_created_at', 'created_at'),
{
'mysql_engine': 'InnoDB',
@ -38,10 +37,12 @@ class InterfaceDefHistory(BaseModel):
id = Column(String(255), primary_key=True, nullable=False, comment='接口定义历史记录ID')
detail = Column(LONGTEXT, comment='接口详细定义JSON格式')
method = Column(String(255), nullable=False, comment='请求方法(GET, POST, PUT, DELETE等)')
project_id = Column(String(255), comment='关联的项目ID')
name = Column(String(255), nullable=False,comment='接口名称')
method = Column(String(255), nullable=False, comment='请求方法(GET, POST)')
is_mobile_ask = Column(Integer, comment='是否为手持端任务(0:否,1:是)')
url = Column(String(255), nullable=False, comment='接口URL')
version = Column(Integer, comment='版本号')
type = Column(Integer, comment='接口类型 (1:公共接口,2:手持端接口)')
def __repr__(self):
return f"<InterfaceDefHistory(id='{self.id}', method='{self.method}', url='{self.url}', version='{self.version}')>"

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@ -7,8 +7,8 @@ Modbus配置模型
"""
import datetime
from sqlalchemy import Column, String, Integer, DateTime
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy import Column, String, Integer, DateTime, Boolean
from sqlalchemy.dialects.mysql import LONGTEXT, DATETIME
from data.models.base import BaseModel
@ -35,7 +35,11 @@ class VWEDModbusConfig(BaseModel):
address_type = Column(String(10), nullable=False, comment='地址类型')
address_number = Column(Integer, nullable=False, comment='地址编号')
task_id = Column(String(255), comment='任务ID')
task_name = Column(String(255), comment='任务名称')
target_value = Column(Integer, comment='目标值')
reset_after_trigger = Column(Boolean, default=False, comment='触发后是否重置')
reset_signal_address = Column(Integer, comment='重置信号地址')
reset_value = Column(Integer, comment='重置值')
remark = Column(String(255), comment='备注')
status = Column(Integer, default=0, comment='状态1:启用, 0:禁用)')
tenant_id = Column(String(255), comment='租户ID')

View File

@ -0,0 +1,127 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
动作点数据模型
基于现有的vwed_operate_point表结构进行扩展
动作点和库位是同一个概念
"""
from sqlalchemy import Column, String, Integer, Boolean, Text, DateTime, ForeignKey, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.mysql import CHAR
from .base import BaseModel
from .storage_area import StorageAreaType
class OperatePoint(BaseModel):
"""
动作点数据模型
继承现有的vwed_operate_point表结构并扩展
"""
__tablename__ = 'vwed_operate_point'
id = Column(CHAR(64), primary_key=True, comment='动作点ID')
station_name = Column(String(64), nullable=False, comment='动作站点名称')
storage_location_name = Column(String(64), nullable=False, comment='库位名称')
scene_id = Column(String(64), nullable=False, comment='场景ID')
# 原有字段
# is_occupied = Column(Boolean, nullable=False, default=False, comment='是否占用')
# is_locked = Column(Boolean, nullable=False, default=False, comment='是否锁定')
is_disabled = Column(Boolean, nullable=False, default=False, comment='是否禁用')
# is_empty_tray = Column(Boolean, nullable=False, default=False, comment='是否空托盘')
content = Column(String(100), nullable=False, default='', comment='内容')
tags = Column(String(100), nullable=False, default='', comment='标签')
# 库区关联
storage_area_id = Column(CHAR(64), ForeignKey('vwed_storage_area.id'), nullable=True, comment='所属库区ID')
# 库区类型
storage_area_type = Column(Enum(StorageAreaType), nullable=True, comment='库区类型')
# 库区名称
area_name = Column(String(64), nullable=True, comment='库区名称')
# 动作点属性
max_layers = Column(Integer, nullable=False, default=1, comment='最大层数')
current_layers = Column(Integer, nullable=False, default=0, comment='当前使用层数')
# 动作点位置信息
position_x = Column(Integer, comment='X坐标', nullable=True)
position_y = Column(Integer, comment='Y坐标', nullable=True)
position_z = Column(Integer, comment='Z坐标', nullable=True)
# 动作点配置
description = Column(Text, comment='动作点描述')
# 关联关系
storage_area = relationship("StorageArea", back_populates="operate_points")
layers = relationship(
"OperatePointLayer",
back_populates="operate_point",
cascade="all, delete-orphan",
order_by="OperatePointLayer.layer_index"
)
def __repr__(self):
return f"<OperatePoint(id={self.id}, station_name={self.station_name}, storage_location_name={self.storage_location_name}, storage_area_id={self.storage_area_id}, storage_area_type={self.storage_area_type}, area_name={self.area_name})>"
def get_available_layers(self):
"""获取可用层数"""
return self.max_layers - self.current_layers
def get_layer_usage_rate(self):
"""获取层使用率"""
if self.max_layers == 0:
return 0.0
return (self.current_layers / self.max_layers) * 100.0
def is_full(self):
"""是否已满"""
return self.current_layers >= self.max_layers
def is_empty(self):
"""是否为空"""
return self.current_layers == 0
def can_store_goods(self):
"""是否可以存放货物"""
return (not self.is_disabled and
not self.is_full() and
self.max_layers > 0)
def can_retrieve_goods(self):
"""是否可以取货"""
return (not self.is_disabled and
not self.is_empty())
def get_occupied_layers(self):
"""获取有货物的层列表"""
return [layer for layer in self.layers if layer.is_occupied]
def get_empty_layers(self):
"""获取空层列表"""
return [layer for layer in self.layers if not layer.is_occupied]
def has_storage_area(self):
"""是否属于某个库区"""
return self.storage_area_id is not None
def get_storage_area_info(self):
"""获取库区信息"""
if self.storage_area_id is None:
return None
return self.storage_area
def is_dense_storage(self):
"""是否为密集库区类型"""
return self.storage_area_type == StorageAreaType.DENSE
def is_general_storage(self):
"""是否为一般库区类型"""
return self.storage_area_type == StorageAreaType.GENERAL
def get_storage_area_display_name(self):
"""获取库区显示名称"""
return self.area_name or "未分配库区"

View File

@ -0,0 +1,179 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
库区分层数据模型
管理每个动作点的分层信息和货物状态
"""
from sqlalchemy import Column, String, Integer, Boolean, Text, DateTime, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.mysql import CHAR
from .base import BaseModel
import datetime
class OperatePointLayer(BaseModel):
"""
库区分层数据模型
每个动作点可以有多个层每层可以独立存放货物
"""
__tablename__ = 'vwed_operate_point_layer'
id = Column(CHAR(64), primary_key=True, comment='层ID')
operate_point_id = Column(CHAR(64), ForeignKey('vwed_operate_point.id'), nullable=False, comment='动作点ID')
station_name = Column(String(64), nullable=False, comment='动作点名称')
storage_location_name = Column(String(64), nullable=False, comment='库位名称')
area_id = Column(CHAR(32), nullable=True, comment='库区ID冗余字段')
area_name = Column(String(64), nullable=True, comment='库区名称')
scene_id = Column(String(64), nullable=False, comment='场景ID冗余字段')
layer_index = Column(Integer, nullable=False, comment='层索引(从1开始)')
layer_name = Column(String(64), comment='层名称')
# 货物状态
is_occupied = Column(Boolean, nullable=False, default=False, comment='是否占用')
goods_content = Column(String(100), nullable=False, default='', comment='货物内容')
goods_weight = Column(Integer, comment='货物重量(克)')
goods_volume = Column(Integer, comment='货物体积(立方厘米)')
# 层状态
is_locked = Column(Boolean, nullable=False, default=False, comment='是否锁定')
is_disabled = Column(Boolean, nullable=False, default=False, comment='是否禁用')
is_empty_tray = Column(Boolean, nullable=False, default=False, comment='是否空托盘')
locked_by = Column(String(128), nullable=True, comment='锁定者')
tags = Column(String(100), nullable=False, default='', comment='标签')
# 层属性
max_weight = Column(Integer, comment='最大承重(克)')
max_volume = Column(Integer, comment='最大体积(立方厘米)')
layer_height = Column(Integer, comment='层高(毫米)')
# 时间信息
goods_stored_at = Column(DateTime, comment='货物存放时间')
goods_retrieved_at = Column(DateTime, comment='货物取出时间')
last_access_at = Column(DateTime, comment='最后访问时间')
# 扩展信息
tags = Column(String(255), comment='层标签', nullable=True)
description = Column(Text, comment='层描述', nullable=True)
config_json = Column(Text, comment='层配置JSON', nullable=True)
# 关联关系
operate_point = relationship("OperatePoint", back_populates="layers")
# 唯一约束:同一个动作点的层索引不能重复
__table_args__ = (
UniqueConstraint('operate_point_id', 'layer_index', name='uk_operate_point_layer'),
)
def __repr__(self):
return f"<OperatePointLayer(id={self.id}, operate_point_id={self.operate_point_id}, station_name={self.station_name}, storage_location_name={self.storage_location_name}, area_id={self.area_id}, area_name={self.area_name}, scene_id={self.scene_id}, layer_index={self.layer_index})>"
def can_store_goods(self, weight=None, volume=None):
"""
检查是否可以存放货物
Args:
weight: 货物重量()
volume: 货物体积(立方厘米)
Returns:
bool: 是否可以存放
"""
if self.is_disabled or self.is_locked or self.is_occupied:
return False
# 检查重量限制
if weight is not None and self.max_weight is not None:
if weight > self.max_weight:
return False
# 检查体积限制
if volume is not None and self.max_volume is not None:
if volume > self.max_volume:
return False
return True
def can_retrieve_goods(self):
"""检查是否可以取货"""
return (not self.is_disabled and
not self.is_locked and
self.is_occupied)
def store_goods(self, content, weight=None, volume=None):
"""
存放货物
Args:
content: 货物内容
weight: 货物重量()
volume: 货物体积(立方厘米)
Returns:
bool: 是否成功
"""
if not self.can_store_goods(weight, volume):
return False
self.is_occupied = True
self.goods_content = content
self.goods_weight = weight
self.goods_volume = volume
self.goods_stored_at = datetime.datetime.now()
self.last_access_at = datetime.datetime.now()
return True
def retrieve_goods(self):
"""
取出货物
Returns:
dict: 货物信息
"""
if not self.can_retrieve_goods():
return None
goods_info = {
'content': self.goods_content,
'weight': self.goods_weight,
'volume': self.goods_volume,
'stored_at': self.goods_stored_at
}
# 清空货物信息
self.is_occupied = False
self.goods_content = ''
self.goods_weight = None
self.goods_volume = None
self.goods_retrieved_at = datetime.datetime.now()
self.last_access_at = datetime.datetime.now()
return goods_info
def get_remaining_capacity(self):
"""获取剩余容量信息"""
result = {}
if self.max_weight is not None:
used_weight = self.goods_weight if self.goods_weight is not None else 0
result['remaining_weight'] = self.max_weight - used_weight
if self.max_volume is not None:
used_volume = self.goods_volume if self.goods_volume is not None else 0
result['remaining_volume'] = self.max_volume - used_volume
return result
def is_overweight(self):
"""检查是否超重"""
if self.max_weight is None or self.goods_weight is None:
return False
return self.goods_weight > self.max_weight
def is_overflow(self):
"""检查是否超体积"""
if self.max_volume is None or self.goods_volume is None:
return False
return self.goods_volume > self.max_volume

View File

@ -0,0 +1,71 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
库区数据模型
支持密集库区和一般库区的管理
"""
from sqlalchemy import Column, String, Integer, Boolean, Text, DateTime, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.mysql import CHAR
from .base import BaseModel
import enum
class StorageAreaType(enum.Enum):
"""库区类型枚举"""
GENERAL = "general" # 一般库区
DENSE = "dense" # 密集库区
class StorageArea(BaseModel):
"""
库区数据模型
一个库区可以包含多个动作点
"""
__tablename__ = 'vwed_storage_area'
id = Column(CHAR(32), primary_key=True, comment='库区ID')
area_name = Column(String(64), nullable=False, unique=True, comment='库区名称')
area_code = Column(String(32), nullable=False, unique=True, comment='库区编码')
area_type = Column(Enum(StorageAreaType), nullable=False, default=StorageAreaType.GENERAL, comment='库区类型')
scene_id = Column(String(32), nullable=False, comment='场景ID')
# 库区属性
max_capacity = Column(Integer, nullable=False, default=0, comment='最大容量')
current_usage = Column(Integer, nullable=False, default=0, comment='当前使用量')
is_active = Column(Boolean, nullable=False, default=True, comment='是否激活')
is_maintenance = Column(Boolean, nullable=False, default=False, comment='是否维护中')
# 库区描述和配置
description = Column(Text, comment='库区描述')
tags = Column(String(255), comment='库区标签')
# 关联关系
operate_points = relationship(
"OperatePoint",
back_populates="storage_area",
cascade="all, delete-orphan"
)
def __repr__(self):
return f"<StorageArea(id={self.id}, area_name={self.area_name}, area_type={self.area_type})>"
def get_available_capacity(self):
"""获取可用容量"""
return self.max_capacity - self.current_usage
def get_usage_rate(self):
"""获取使用率"""
if self.max_capacity == 0:
return 0.0
return (self.current_usage / self.max_capacity) * 100.0
def is_dense_area(self):
"""是否为密集库区"""
return self.area_type == StorageAreaType.DENSE
def is_general_area(self):
"""是否为一般库区"""
return self.area_type == StorageAreaType.GENERAL

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
库位操作记录模型
记录库位相关的操作日志
"""
from sqlalchemy import Column, String, Text, DateTime, func
from sqlalchemy.dialects.postgresql import UUID
from data.models.base import BaseModel
import uuid
class StorageLocationLog(BaseModel):
"""
库位操作记录表
记录库位相关的所有操作
"""
__tablename__ = "storage_location_logs"
# 主键ID
# id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, comment="记录ID")
# 操作时间
operation_time = Column(DateTime, nullable=False, default=func.now(), comment="操作时间")
# 操作人
operator = Column(String(100), nullable=False, comment="操作人")
# 操作类型/功能
operation_type = Column(String(50), nullable=False, comment="操作类型")
# 影响的库位列表JSON格式存储
affected_storage_locations = Column(Text, nullable=False, comment="影响的库位列表JSON格式")
# 操作描述
description = Column(String(500), nullable=True, comment="操作描述")
def __repr__(self):
return f"<StorageLocationLog(id={self.id}, operator={self.operator}, operation_type={self.operation_type})>"

View File

@ -39,7 +39,6 @@ class VWEDTaskLog(BaseModel):
task_block_id = Column(String(255), comment='任务块ID')
task_id = Column(String(255), comment='对应的任务定义ID')
task_record_id = Column(String(255), comment='对应的任务执行记录ID')
block_record_id = Column(String(255), comment='对应的任务块执行记录ID')
def __repr__(self):
return f"<VWEDTaskLog(id='{self.id}', level='{self.level}')>"

View File

@ -22,6 +22,12 @@ from config.settings import settings
from utils.logger import get_logger
logger = get_logger("data.session")
# 关闭SQLAlchemy日志输出
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.dialects').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING)
# 创建数据库引擎
engine = create_engine(
DBConfig.DATABASE_URL,

199
docs/alert_sync.md Normal file
View File

@ -0,0 +1,199 @@
# 告警同步功能使用说明
## 功能概述
告警同步功能会自动将您的VWED任务微服务中产生的WARNING及以上级别的日志同步到主系统实现统一的告警管理。
## 核心特性
- **自动同步**: 无需修改现有业务代码所有WARNING及以上级别的日志会自动同步
- **异步处理**: 采用异步队列,不影响业务性能
- **智能分类**: 根据logger名称自动分类告警类型系统/机器人/任务)
- **错误重试**: 支持配置重试次数和延迟
- **灵活配置**: 支持开关控制和详细参数配置
## 告警级别映射
| Python日志级别 | 主系统告警级别 | 说明 |
|---------------|---------------|------|
| logging.WARNING | 2 (警告) | 需要关注但不影响系统运行 |
| logging.ERROR | 3 (错误) | 影响功能但系统可继续运行 |
| logging.CRITICAL | 4 (严重) | 严重错误,需要立即处理 |
| logging.INFO | - | 不同步(信息级别) |
## 告警类型自动识别
系统会根据logger名称自动识别告警类型
| Logger名称包含关键词 | 告警类型 | 主系统type值 |
|-------------------|---------|-------------|
| task, execution, scheduler, template | 任务告警 | 3 |
| robot, vehicle, amr | 机器人告警 | 2 |
| 其他 | 系统告警 | 1 |
## 配置说明
`config/settings.py`中的相关配置项:
```python
# 告警同步配置
ALERT_SYNC_ENABLED: bool = True # 是否启用告警同步
ALERT_SYNC_HOST: str = "192.168.189.97" # 主系统IP
ALERT_SYNC_PORT: int = 8080 # 主系统端口
ALERT_SYNC_API_PATH: str = "/warning" # 告警API路径
ALERT_SYNC_TIMEOUT: int = 10 # 请求超时时间(秒)
ALERT_SYNC_RETRY_COUNT: int = 3 # 重试次数
ALERT_SYNC_RETRY_DELAY: int = 1 # 重试延迟(秒)
ALERT_SYNC_BATCH_SIZE: int = 10 # 批量发送大小
ALERT_SYNC_QUEUE_SIZE: int = 1000 # 队列大小
ALERT_SYNC_MIN_LEVEL: str = "WARNING" # 最小告警级别
```
### 环境变量配置
也可以通过环境变量配置:
```bash
export ALERT_SYNC_ENABLED=true
export ALERT_SYNC_HOST=192.168.189.97
export ALERT_SYNC_PORT=8080
export ALERT_SYNC_TIMEOUT=10
```
## 使用方法
### 1. 基本使用
直接使用现有的日志记录方式,系统会自动同步:
```python
from utils.logger import get_logger
logger = get_logger("services.task_service")
# 这些日志会自动同步到主系统
logger.warning("任务执行超时")
logger.error("任务执行失败")
logger.critical("任务调度器严重错误")
# 这些日志不会同步
logger.info("任务开始执行")
logger.debug("调试信息")
```
### 2. 异常处理
使用exception方法记录异常会包含更详细的信息
```python
try:
# 业务逻辑
result = some_risky_operation()
except Exception as e:
logger.exception("操作失败") # 会自动同步并包含异常堆栈
```
### 3. 不同模块的告警
```python
# 任务相关告警
task_logger = get_logger("services.task_execution")
task_logger.error("任务执行失败") # 会被标记为任务告警(type=3)
# 机器人相关告警
robot_logger = get_logger("services.robot_scheduler")
robot_logger.warning("机器人电池电量低") # 会被标记为机器人告警(type=2)
# 系统相关告警
system_logger = get_logger("database.connection")
system_logger.error("数据库连接失败") # 会被标记为系统告警(type=1)
```
## 自动处理的场景
以下场景的错误会自动记录日志并同步:
1. **HTTP错误**: 4xx和5xx HTTP状态码
2. **参数验证错误**: FastAPI参数验证失败
3. **未捕获异常**: 全局异常处理器捕获的异常
4. **业务逻辑错误**: 您在代码中主动记录的WARNING及以上级别日志
## 同步到主系统的数据格式
发送到主系统的数据格式如下:
```json
{
"type": 3, // 告警类型: 1-系统, 2-机器人, 3-任务
"level": 2, // 告警级别: 2-警告, 3-错误, 4-严重
"code": 5678, // 4位告警码5400-9999范围自动生成
"name": "TASK_SERVICE_WARNING", // 告警名称(自动生成)
"description": "任务执行超时 [文件: task_service.py:123] [函数: execute_task()]",
"solution": "请检查相关日志文件获取详细的异常堆栈信息,并根据异常类型进行排查" // 可选
}
```
## 测试功能
运行测试脚本验证功能:
```bash
cd scripts
python test_alert_sync.py
```
测试脚本会:
- 发送不同类型和级别的告警
- 验证INFO级别不会同步
- 显示服务状态信息
## 故障排查
### 1. 检查配置
```python
from utils.alert_sync import get_alert_sync_service
service = get_alert_sync_service()
print(f"启用状态: {service.enabled}")
print(f"同步URL: {service.sync_url}")
print(f"队列大小: {service.alert_queue.qsize()}")
```
### 2. 查看日志
告警同步的错误信息会输出到控制台,查看是否有网络或配置问题。
### 3. 临时禁用
如果需要临时禁用告警同步:
```bash
export ALERT_SYNC_ENABLED=false
```
或在配置文件中设置:
```python
ALERT_SYNC_ENABLED: bool = False
```
## 性能影响
- **内存占用**: 队列最大1000个待发送告警每个告警约1KB
- **网络开销**: 异步发送,不阻塞业务逻辑
- **CPU占用**: 后台线程处理,对主业务影响极小
## 注意事项
1. **网络依赖**: 需要确保微服务能够访问主系统的告警接口
2. **队列满载**: 如果队列满载,新的告警会被丢弃
3. **重试机制**: 发送失败会自动重试,但最终失败的告警会丢失
4. **日志循环**: 告警同步本身的错误不会触发新的告警,避免无限循环
## 最佳实践
1. **合理使用日志级别**: 只有真正需要关注的问题才使用WARNING及以上级别
2. **提供有用信息**: 在日志消息中包含足够的上下文信息
3. **监控队列状态**: 定期检查队列大小,避免积压
4. **测试网络连通**: 部署前测试到主系统的网络连通性

84169
logs/app.log

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

36
middlewares/__init__.py Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
中间件模块初始化文件
提供统一的中间件注册方法
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from middlewares.request_logger import register_middleware as register_request_logger
from middlewares.error_handlers import register_exception_handlers
from config.settings import settings
def register_middlewares(app: FastAPI):
"""
注册所有中间件到FastAPI应用
Args:
app: FastAPI应用实例
"""
# 添加CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=settings.CORS_ALLOW_CREDENTIALS,
allow_methods=settings.CORS_ALLOW_METHODS,
allow_headers=settings.CORS_ALLOW_HEADERS,
)
# 注册请求日志中间件
register_request_logger(app)
# 注册异常处理器
register_exception_handlers(app)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,130 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
错误处理中间件模块
提供全局异常处理和错误响应格式化
"""
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
import traceback
from utils.logger import get_logger
from routes.common_api import format_response
from config.error_messages import VALIDATION_ERROR_MESSAGES, HTTP_ERROR_MESSAGES
from config.settings import settings
# 设置日志
logger = get_logger("middleware.error_handlers")
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
处理验证错误将错误消息转换为中文并提供更友好的错误提示
包括显示具体缺失的字段名称
"""
errors = exc.errors()
error_details = []
missing_fields = []
for error in errors:
error_type = error.get("type", "")
loc = error.get("loc", [])
# 获取完整的字段路径排除body/query等
if len(loc) > 1 and loc[0] in ["body", "query", "path", "header"]:
field_path = ".".join(str(item) for item in loc[1:])
else:
field_path = ".".join(str(item) for item in loc)
# 获取中文错误消息
message = VALIDATION_ERROR_MESSAGES.get(error_type, error.get("msg", "验证错误"))
# 替换消息中的参数
context = error.get("ctx", {})
for key, value in context.items():
message = message.replace(f"{{{key}}}", str(value))
# 收集缺失字段
if error_type == "missing" or error_type == "value_error.missing":
missing_fields.append(field_path)
error_details.append({
"field": field_path,
"message": message,
"type": error_type
})
# 构建友好的错误响应
if missing_fields:
missing_fields_str = ", ".join(missing_fields)
error_message = f"缺少必填字段: {missing_fields_str}"
elif error_details:
# 提取第一个错误的字段和消息
first_error = error_details[0]
error_message = f"参数 '{first_error['field']}' 验证失败: {first_error['message']}"
else:
error_message = "参数验证失败"
# 记录参数验证错误,以便同步到主系统
logger.warning(f"参数验证失败: {error_message} - 请求路径: {request.url.path}")
return JSONResponse(
status_code=400,
content={
"code": 400,
"message": error_message,
"data": error_details if len(error_details) > 1 else None
}
)
async def http_exception_handler(request: Request, exc: HTTPException):
"""处理HTTP异常转换为统一的响应格式"""
status_code = exc.status_code
# 获取错误消息,优先使用自定义消息,否则使用配置中的错误消息
message = exc.detail
if isinstance(message, str) and message == "Not Found":
message = HTTP_ERROR_MESSAGES.get(status_code, message)
# 记录HTTP错误日志以便同步到主系统
if status_code >= 500:
# 5xx错误记录为ERROR级别
logger.error(f"HTTP {status_code} 错误: {message} - 请求路径: {request.url.path}")
elif status_code >= 400:
# 4xx错误记录为WARNING级别
logger.warning(f"HTTP {status_code} 警告: {message} - 请求路径: {request.url.path}")
return JSONResponse(
status_code=status_code,
content=format_response(
code=status_code,
message=message,
data=None
)
)
async def global_exception_handler(request: Request, exc: Exception):
"""处理所有未捕获的异常"""
logger.error(f"未捕获异常: {str(exc)}")
logger.error(traceback.format_exc())
return JSONResponse(
status_code=500,
content=format_response(
code=500,
message="服务器内部错误,请联系管理员",
data=None if not settings.DEBUG else str(exc)
)
)
def register_exception_handlers(app):
"""
注册所有异常处理器到FastAPI应用
Args:
app: FastAPI应用实例
"""
app.exception_handler(RequestValidationError)(validation_exception_handler)
app.exception_handler(HTTPException)(http_exception_handler)
app.exception_handler(Exception)(global_exception_handler)

View File

@ -0,0 +1,74 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
请求日志中间件模块
提供请求日志记录功能
"""
import time
import traceback
from fastapi import Request
from fastapi.responses import JSONResponse
from utils.logger import get_logger
from routes.common_api import format_response
# 设置日志
logger = get_logger("middleware.request_logger")
async def log_requests(request: Request, call_next):
"""
记录请求日志的中间件
Args:
request: 请求对象
call_next: 下一个中间件或路由处理函数
Returns:
响应对象
"""
start_time = time.time()
# 获取请求信息
method = request.method
url = request.url.path
client_host = request.client.host if request.client else "unknown"
# 记录请求
logger.info(f"请求开始: {method} {url} 来自 {client_host}")
try:
# 处理请求
response = await call_next(request)
# 计算处理时间
process_time = time.time() - start_time
logger.info(f"请求完成: {method} {url} 状态码: {response.status_code} 耗时: {process_time:.4f}")
return response
except Exception as e:
# 记录异常
process_time = time.time() - start_time
logger.error(f"请求异常: {method} {url} 耗时: {process_time:.4f}")
logger.error(f"异常详情: {str(e)}")
logger.error(traceback.format_exc())
# 返回通用错误响应
return JSONResponse(
status_code=500,
content=format_response(
code=500,
message="服务器内部错误,请联系管理员",
data=None
)
)
def register_middleware(app):
"""
注册请求日志中间件到FastAPI应用
Args:
app: FastAPI应用实例
"""
app.middleware("http")(log_requests)

View File

@ -0,0 +1,98 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
创建扩展属性表
Revision ID: 002
Revises: 001
Create Date: 2024-12-20 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.mysql import VARCHAR, TEXT, DATETIME, ENUM
# revision identifiers, used by Alembic.
revision = '002'
down_revision = '001'
branch_labels = None
depends_on = None
def upgrade():
"""创建扩展属性表"""
# 创建扩展属性表
op.create_table(
'extended_properties',
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True, comment='主键ID'),
sa.Column('created_at', DATETIME(fsp=6), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP(6)'), comment='创建时间'),
sa.Column('updated_at', DATETIME(fsp=6), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)'), comment='更新时间'),
sa.Column('is_deleted', sa.Boolean, nullable=False, default=False, comment='是否删除(软删除标记)'),
# 基本信息
sa.Column('property_key', VARCHAR(200), nullable=False, comment='属性键(与属性名称相同)'),
sa.Column('property_name', VARCHAR(200), nullable=False, comment='属性名称(唯一标识)'),
sa.Column('property_type', ENUM('string', 'integer', 'float', 'boolean', 'date', 'datetime', 'text', 'select', 'multiselect'), nullable=False, comment='属性类型'),
# 属性设置
sa.Column('is_required', sa.Boolean, nullable=False, default=False, comment='是否必填'),
sa.Column('is_enabled', sa.Boolean, nullable=False, default=True, comment='是否启用'),
# 描述信息
sa.Column('description', TEXT, comment='属性描述'),
sa.Column('placeholder', VARCHAR(500), comment='输入提示'),
# 默认值和选项
sa.Column('default_value', TEXT, comment='默认值'),
sa.Column('options', TEXT, comment='选项值JSON格式用于select和multiselect类型'),
# 验证规则
sa.Column('validation_rules', TEXT, comment='验证规则JSON格式'),
# 分类和排序
sa.Column('category', VARCHAR(100), comment='属性分类'),
sa.Column('sort_order', sa.Integer, nullable=False, default=0, comment='排序顺序'),
# 显示设置
sa.Column('display_width', sa.Integer, nullable=False, default=200, comment='显示宽度(像素)'),
sa.Column('display_format', VARCHAR(100), comment='显示格式'),
mysql_engine='InnoDB',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_unicode_ci',
comment='扩展属性表'
)
# 创建索引
op.create_index('idx_extended_properties_property_key', 'extended_properties', ['property_key'])
op.create_index('idx_extended_properties_property_name', 'extended_properties', ['property_name'])
op.create_index('idx_extended_properties_property_type', 'extended_properties', ['property_type'])
op.create_index('idx_extended_properties_category', 'extended_properties', ['category'])
op.create_index('idx_extended_properties_is_enabled', 'extended_properties', ['is_enabled'])
op.create_index('idx_extended_properties_is_deleted', 'extended_properties', ['is_deleted'])
op.create_index('idx_extended_properties_sort_order', 'extended_properties', ['sort_order'])
# 创建唯一约束
op.create_unique_constraint('uq_extended_properties_property_key', 'extended_properties', ['property_key'])
op.create_unique_constraint('uq_extended_properties_property_name', 'extended_properties', ['property_name'])
def downgrade():
"""删除扩展属性表"""
# 删除索引
op.drop_index('idx_extended_properties_property_key', 'extended_properties')
op.drop_index('idx_extended_properties_property_name', 'extended_properties')
op.drop_index('idx_extended_properties_property_type', 'extended_properties')
op.drop_index('idx_extended_properties_category', 'extended_properties')
op.drop_index('idx_extended_properties_is_enabled', 'extended_properties')
op.drop_index('idx_extended_properties_is_deleted', 'extended_properties')
op.drop_index('idx_extended_properties_sort_order', 'extended_properties')
# 删除唯一约束
op.drop_constraint('uq_extended_properties_property_key', 'extended_properties', type_='unique')
op.drop_constraint('uq_extended_properties_property_name', 'extended_properties', type_='unique')
# 删除表
op.drop_table('extended_properties')

View File

@ -14,6 +14,7 @@ pydantic
pydantic-settings
python-multipart
aiohttp
websockets
# 工具库
python-dotenv

View File

@ -1,3 +1,51 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
路由模块初始化文件
集中管理所有API路由的导入
"""
# 导入所有路由模块
from routes.database import router as db_router
from routes.template_api import router as template_router
from routes.task_api import router as task_router
from routes.common_api import router as common_router
from routes.task_edit_api import router as task_edit_router
from routes.script_api import router as script_router
from routes.task_record_api import router as task_record_router
from routes.calldevice_api import router as calldevice_router
from routes.modbus_config_api import router as modbus_config_router
from routes.websocket_api import router as websocket_router
from routes.map_data_api import router as map_data_router
from routes.operate_point_api import router as operate_point_router
# 路由列表,按照注册顺序排列
routers = [
common_router,
db_router,
template_router,
task_router,
task_edit_router,
script_router,
task_record_router,
calldevice_router,
modbus_config_router,
websocket_router,
map_data_router,
operate_point_router
]
def register_routers(app):
"""
注册所有路由到FastAPI应用
Args:
app: FastAPI应用实例
"""
for router in routers:
app.include_router(router)
# from fastapi import FastAPI
# # 导入所有路由文件

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

459
routes/calldevice_api.py Normal file
View File

@ -0,0 +1,459 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
呼叫器设备API模块
提供呼叫器设备相关的API接口
"""
from typing import Dict, List, Any, Optional
from fastapi import APIRouter, Body, Query, Path, Request, File, UploadFile, Form, Response
from pydantic import BaseModel
from routes.common_api import format_response, error_response
from utils.logger import get_logger
from services.calldevice_service import CallDeviceService
from routes.model.calldevice_model import CallDeviceModel, DeleteDeviceRequest, ExportDevicesRequest
from utils.crypto_utils import CryptoUtils
import time
import random
import json
# 创建路由
router = APIRouter(
prefix="/api/vwed-calldevice",
tags=["VWED呼叫器设备"]
)
# 设置日志
logger = get_logger("app.calldevice_api")
@router.post("/add")
async def add_call_device(
call_device: CallDeviceModel = Body(..., description="呼叫器设备信息"),
request: Request = Request
):
"""
新增呼叫器设备
Args:
call_device: 呼叫器设备信息包括协议类型品牌IP端口设备名称状态及按钮配置
request: 请求对象
Returns:
包含新增结果的响应
"""
try:
# 将按钮列表转换为字典列表,映射字段名称
buttons_dict = []
api_token = request.headers.get("x-access-token")
if call_device.buttons:
for button in call_device.buttons:
button_dict = {
"signal_name": button.signal_name,
"signal_type": button.signal_type,
"signal_length": button.signal_length,
"register_address": button.register_address,
"function_code": button.function_code,
"remark": button.remark,
"vwed_task_id": button.vwed_task_id,
"long_vwed_task_id": button.long_vwed_task_id
}
buttons_dict.append(button_dict)
result = await CallDeviceService.add_call_device(
protocol=call_device.protocol,
brand=call_device.brand,
ip=call_device.ip,
port=call_device.port,
device_name=call_device.device_name,
status=call_device.status,
slave_id=call_device.slave_id,
buttons=buttons_dict,
api_token=api_token
)
if not result.get("success", False):
return error_response(
message=result.get("message", "新增呼叫器设备失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "新增呼叫器设备成功")
)
except Exception as e:
logger.error(f"新增呼叫器设备异常: {str(e)}")
return error_response(message=f"新增呼叫器设备失败: {str(e)}", code=500)
@router.put("/update/{device_id}")
async def update_call_device(
device_id: str = Path(..., description="设备ID"),
call_device: CallDeviceModel = Body(..., description="呼叫器设备信息")
):
"""
更新呼叫器设备
Args:
device_id: 设备ID
call_device: 呼叫器设备信息包括协议类型品牌IP端口设备名称状态及按钮配置
Returns:
包含更新结果的响应
"""
try:
# 将按钮列表转换为字典列表,映射字段名称
buttons_dict = []
if call_device.buttons:
for button in call_device.buttons:
button_dict = {
"signal_name": button.signal_name,
"signal_type": button.signal_type,
"signal_length": button.signal_length,
"register_address": button.register_address,
"function_code": button.function_code,
"remark": button.remark,
"vwed_task_id": button.vwed_task_id,
"long_vwed_task_id": button.long_vwed_task_id
}
buttons_dict.append(button_dict)
result = await CallDeviceService.update_call_device(
device_id=device_id,
protocol=call_device.protocol,
brand=call_device.brand,
ip=call_device.ip,
port=call_device.port,
device_name=call_device.device_name,
status=call_device.status,
slave_id=call_device.slave_id,
buttons=buttons_dict
)
if not result.get("success", False):
return error_response(
message=result.get("message", "更新呼叫器设备失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "更新呼叫器设备成功")
)
except Exception as e:
logger.error(f"更新呼叫器设备异常: {str(e)}")
return error_response(message=f"更新呼叫器设备失败: {str(e)}", code=500)
@router.post("/batch-delete")
async def delete_call_device_batch(
request: DeleteDeviceRequest = Body(..., description="批量删除设备请求")
):
"""
删除呼叫器设备
Args:
request: 包含要删除的设备ID列表的请求
Returns:
包含删除结果的响应
"""
try:
if not request.ids:
return error_response(
message="设备ID列表不能为空",
code=400
)
result = await CallDeviceService.delete_call_device(device_ids=request.ids)
if not result.get("success", False):
return error_response(
message=result.get("message", "批量删除呼叫器设备失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "批量删除呼叫器设备成功")
)
except Exception as e:
logger.error(f"批量删除呼叫器设备异常: {str(e)}")
return error_response(message=f"批量删除呼叫器设备失败: {str(e)}", code=500)
@router.get("/list")
async def get_call_device_list(
page: int = Query(1, description="页码", ge=1),
page_size: int = Query(10, description="每页数量", ge=1, le=100),
device_name: str = Query(None, description="设备名称搜索"),
ip: str = Query(None, description="IP地址搜索"),
protocol: str = Query(None, description="协议类型搜索"),
status: int = Query(None, description="状态过滤(0:禁用,1:启用)")
):
"""
获取呼叫器设备列表
Args:
page: 页码
page_size: 每页数量
device_name: 设备名称搜索
ip: IP地址搜索
protocol: 协议类型搜索
status: 状态过滤(0:禁用,1:启用)
Returns:
包含设备列表和分页信息的响应
"""
try:
result = await CallDeviceService.get_call_device_list(
page=page,
page_size=page_size,
device_name=device_name,
ip=ip,
protocol=protocol,
status=status
)
if not result.get("success", False):
return error_response(
message=result.get("message", "获取呼叫器设备列表失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "获取呼叫器设备列表成功")
)
except Exception as e:
logger.error(f"获取呼叫器设备列表异常: {str(e)}")
return error_response(message=f"获取呼叫器设备列表失败: {str(e)}", code=500)
@router.get("/detail/{device_id}")
async def get_call_device_detail(
device_id: str = Path(..., description="设备ID")
):
"""
获取呼叫器设备详情
Args:
device_id: 设备ID
Returns:
包含设备详情的响应
"""
try:
result = await CallDeviceService.get_call_device_detail(device_id=device_id)
if not result.get("success", False):
return error_response(
message=result.get("message", "获取呼叫器设备详情失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "获取呼叫器设备详情成功")
)
except Exception as e:
logger.error(f"获取呼叫器设备详情异常: {str(e)}")
return error_response(message=f"获取呼叫器设备详情失败: {str(e)}", code=500)
@router.post("/export-batch")
async def export_batch_calldevice(
export_req: ExportDevicesRequest = Body(..., description="批量导出设备请求")
):
"""
批量导出呼叫器设备
Args:
export_req: 导出请求数据包含要导出的设备ID列表
Returns:
Response: 包含设备配置的加密专有格式文件
"""
try:
result = await CallDeviceService.export_call_devices(device_ids=export_req.ids)
if not result.get("success", False):
return error_response(
message=result.get("message", "批量导出呼叫器设备失败"),
code=400
)
# 获取设备数据
devices_data = result.get("data", [])
# 加密数据并添加文件头、签名和校验和
encrypted_data = CryptoUtils.encrypt_data(devices_data)
# 使用专有文件扩展名 .vdex (VWED Device Export)
filename = f"calldevices_export_{len(export_req.ids)}.vdex" if len(export_req.ids) > 1 else f"calldevice_{export_req.ids[0]}.vdex"
# 返回加密的二进制文件
headers = {
'Content-Disposition': f'attachment; filename={filename}',
'Content-Type': 'application/octet-stream',
'X-Content-Type-Options': 'nosniff', # 防止浏览器嗅探文件类型
"Access-Control-Expose-Headers": "Content-Disposition"
}
return Response(content=encrypted_data, media_type='application/octet-stream', headers=headers)
except Exception as e:
logger.error(f"批量导出呼叫器设备异常: {str(e)}")
return error_response(message=f"批量导出呼叫器设备失败: {str(e)}", code=500)
@router.post("/import")
async def import_calldevice(
file: UploadFile = File(..., description="呼叫器设备配置文件,加密专有格式")
):
"""
导入呼叫器设备
Args:
file: 设备配置文件加密专有格式
Returns:
包含导入结果的响应
"""
try:
# 读取文件内容
content = await file.read()
try:
# 解密并验证数据
devices_data = CryptoUtils.decrypt_data(content)
# 生成唯一后缀以避免名称冲突
timestamp = int(time.time())
rand_num = random.randint(1000, 9999)
rename_suffix = f"-备份-{timestamp}{rand_num}"
# 导入设备
result = await CallDeviceService.import_call_devices(
devices_data=devices_data,
rename_suffix=rename_suffix
)
if not result.get("success", False):
return error_response(
message=result.get("message", "导入呼叫器设备失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "导入呼叫器设备成功")
)
except ValueError as e:
logger.error(f"文件格式无效或已损坏: {str(e)}")
return error_response(f"无法导入呼叫器设备: {str(e)}", 400)
except Exception as e:
logger.error(f"解析呼叫器设备数据失败: {str(e)}")
return error_response(f"解析呼叫器设备数据失败: {str(e)}", 400)
except Exception as e:
logger.error(f"导入呼叫器设备异常: {str(e)}")
return error_response(message=f"导入呼叫器设备失败: {str(e)}", code=500)
@router.post("/monitor/start/{device_id}")
async def start_device_monitor(
device_id: str = Path(..., description="设备ID"),
request: Request = Request
):
"""
启动设备监控
开启一个监控线程持续监控设备按钮状态当检测到按钮按下时触发对应任务
Args:
device_id: 设备ID
Returns:
包含启动结果的响应
"""
try:
# 获取token用于同步到系统任务
tf_api_token = request.headers.get("x-access-token")
result = await CallDeviceService.start_device_monitor(device_id, tf_api_token)
if not result.get("success", False):
return error_response(
message=result.get("message", "启动设备监控失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "启动设备监控成功")
)
except Exception as e:
logger.error(f"启动设备监控异常: {str(e)}")
return error_response(message=f"启动设备监控失败: {str(e)}", code=500)
@router.post("/monitor/stop/{device_id}")
async def stop_device_monitor(
device_id: str = Path(..., description="设备ID")
):
"""
停止设备监控
停止设备的监控线程不再监控按钮状态
Args:
device_id: 设备ID
Returns:
包含停止结果的响应
"""
try:
result = await CallDeviceService.stop_device_monitor(device_id)
if not result.get("success", False):
return error_response(
message=result.get("message", "停止设备监控失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "停止设备监控成功")
)
except Exception as e:
logger.error(f"停止设备监控异常: {str(e)}")
return error_response(message=f"停止设备监控失败: {str(e)}", code=500)
@router.get("/monitor/status/{device_id}")
async def get_device_monitor_status(
device_id: str = Path(..., description="设备ID")
):
"""
获取设备监控状态
获取指定设备的监控状态信息
Args:
device_id: 设备ID
Returns:
包含设备监控状态的响应
"""
try:
result = await CallDeviceService.get_device_monitor_status(device_id)
if not result.get("success", False):
return error_response(
message=result.get("message", "获取设备监控状态失败"),
code=400
)
return format_response(
data=result.get("data", {}),
message=result.get("message", "获取设备监控状态成功")
)
except Exception as e:
logger.error(f"获取设备监控状态异常: {str(e)}")
return error_response(message=f"获取设备监控状态失败: {str(e)}", code=500)

202
routes/map_data_api.py Normal file
View File

@ -0,0 +1,202 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
地图数据推送API路由
实现地图数据推送时的动作点和库区数据存储功能
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Any, Dict
from data.session import get_db
from services.map_data_service import MapDataService
from routes.model.base import ApiResponse
from routes.model.map_model import (
MapDataPushRequest, MapDataPushResponse,
MapDataQueryRequest, MapDataQueryResponse
)
from routes.common_api import format_response, error_response
from utils.logger import get_logger
# 创建路由
router = APIRouter(prefix="/api/vwed-map-data", tags=["地图数据推送"])
# 设置日志
logger = get_logger("app.map_data_api")
# 标准API响应格式
def api_response(code: int = 200, message: str = "操作成功", data: Any = None) -> Dict[str, Any]:
"""
标准API响应格式
Args:
code: 状态码
message: 响应消息
data: 响应数据
Returns:
Dict[str, Any]: 格式化的响应数据
"""
return {
"code": code,
"message": message,
"data": data
}
@router.post("/push", response_model=ApiResponse[MapDataPushResponse])
async def push_map_data(
request: MapDataPushRequest,
db: Session = Depends(get_db)
):
"""
推送地图数据
当用户推送新的地图时将地图相关的动作点和库区数据存入数据库
支持库区分类密集库区一般库区和动作点分层存储
重要功能
- 自动为新创建的库位层同步所有已启用的扩展属性
- 新库位层会自动包含系统中定义的扩展属性配置
- 扩展属性使用默认值进行初始化
Args:
request: 地图数据推送请求
db: 数据库会话
Returns:
ApiResponse[MapDataPushResponse]: 推送结果响应
"""
try:
# 调用服务层方法推送地图数据
result = MapDataService.push_map_data(db=db, request=request)
return api_response(message="地图数据推送成功", data=result)
except ValueError as e:
# 数据验证错误
return error_response(str(e), 400)
except Exception as e:
logger.error(f"地图数据推送失败: {str(e)}")
return error_response(f"地图数据推送失败: {str(e)}", 500)
@router.post("/query", response_model=ApiResponse[MapDataQueryResponse])
async def query_map_data(
request: MapDataQueryRequest,
db: Session = Depends(get_db)
):
"""
查询地图数据
根据场景ID查询地图中的库区和动作点数据
支持按库区类型筛选和是否包含分层数据的选项
Args:
request: 地图数据查询请求
db: 数据库会话
Returns:
ApiResponse[MapDataQueryResponse]: 查询结果响应
"""
try:
# 调用服务层方法查询地图数据
result = MapDataService.query_map_data(db=db, request=request)
return api_response(message="查询成功", data=result)
except ValueError as e:
# 数据验证错误
return error_response(str(e), 400)
except Exception as e:
logger.error(f"查询地图数据失败: {str(e)}")
return error_response(f"查询地图数据失败: {str(e)}", 500)
@router.delete("/scene/{scene_id}")
async def delete_scene_data(
scene_id: str,
db: Session = Depends(get_db)
):
"""
删除场景数据
删除指定场景的所有地图数据包括库区动作点和分层数据
Args:
scene_id: 场景ID
db: 数据库会话
Returns:
ApiResponse: 删除结果响应
"""
try:
# 调用服务层的删除方法
MapDataService._delete_existing_data(db=db, scene_id=scene_id)
db.commit()
logger.info(f"场景数据删除成功: 场景ID={scene_id}")
return api_response(message="场景数据删除成功")
except Exception as e:
db.rollback()
logger.error(f"删除场景数据失败: {str(e)}")
return error_response(f"删除场景数据失败: {str(e)}", 500)
@router.get("/scene/{scene_id}/summary")
async def get_scene_summary(
scene_id: str,
db: Session = Depends(get_db)
):
"""
获取场景数据摘要
获取指定场景的数据统计信息包括库区和动作点的数量统计
Args:
scene_id: 场景ID
db: 数据库会话
Returns:
ApiResponse: 场景摘要信息
"""
try:
# 构造查询请求
query_request = MapDataQueryRequest(
scene_id=scene_id,
include_layers=True
)
# 调用服务层方法获取数据
result = MapDataService.query_map_data(db=db, request=query_request)
# 提取摘要信息
summary = {
"scene_id": result.scene_id,
"total_storage_areas": len(result.storage_areas),
"total_operate_points": len(result.operate_points),
"total_capacity": result.total_capacity,
"used_capacity": result.used_capacity,
"capacity_usage_rate": round(
(result.used_capacity / result.total_capacity * 100) if result.total_capacity > 0 else 0, 2
),
"dense_areas_count": result.dense_areas_count,
"general_areas_count": result.general_areas_count,
"total_layers": result.total_layers,
"occupied_layers": result.occupied_layers,
"layer_usage_rate": round(
(result.occupied_layers / result.total_layers * 100) if result.total_layers > 0 else 0, 2
)
}
return api_response(message="获取场景摘要成功", data=summary)
except Exception as e:
logger.error(f"获取场景摘要失败: {str(e)}")
return error_response(f"获取场景摘要失败: {str(e)}", 500)

189
routes/modbus_config_api.py Normal file
View File

@ -0,0 +1,189 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Modbus配置API模块
提供Modbus配置相关的API接口
"""
from typing import Dict, List, Any, Optional
from fastapi import APIRouter, Query, Path, Request, Body
from routes.common_api import format_response, error_response
from utils.logger import get_logger
from services.modbus_config_service import ModbusConfigService
from routes.model.modbus_config_model import (
ModbusConfigAddRequest,
ModbusConfigUpdateRequest,
ModbusConnectionTestRequest,
ModbusConfigListParams
)
# 创建路由
router = APIRouter(
prefix="/api/vwed-modbus-config",
tags=["VWED Modbus配置"]
)
# 设置日志
logger = get_logger("app.modbus_config_api")
@router.post("/add")
async def add_modbus_config(
request: Request,
config_data: ModbusConfigAddRequest = Body(..., description="Modbus配置信息")
):
"""
新增Modbus配置
Args:
config_data: Modbus配置信息包含nameipport等字段
Returns:
包含新增结果的响应
"""
try:
# 转换为字典
config_dict = config_data.model_dump(exclude_unset=True)
result = await ModbusConfigService.add_modbus_config(config_dict)
if not result["success"]:
return error_response(message=result["message"], code=400)
return format_response(data=result["data"], message=result["message"])
except Exception as e:
logger.error(f"新增Modbus配置失败: {str(e)}")
return error_response(message=f"新增Modbus配置失败: {str(e)}", code=500)
@router.delete("/{config_id}")
async def delete_modbus_config(
config_id: str = Path(..., description="配置ID")
):
"""
删除Modbus配置
Args:
config_id: Modbus配置ID
Returns:
包含删除结果的响应
"""
try:
result = await ModbusConfigService.delete_modbus_config(config_id)
if not result["success"]:
return error_response(message=result["message"], code=404)
return format_response(message=result["message"])
except Exception as e:
logger.error(f"删除Modbus配置失败: {str(e)}")
return error_response(message=f"删除Modbus配置失败: {str(e)}", code=500)
@router.post("/test-connection")
async def test_modbus_connection(
connection_data: ModbusConnectionTestRequest = Body(..., description="Modbus连接参数")
):
"""
测试Modbus连接
Args:
connection_data: Modbus连接参数包含ipportslave_id等
Returns:
包含连接测试结果的响应
"""
try:
# 转换为字典
connection_dict = connection_data.model_dump(exclude_unset=True)
result = await ModbusConfigService.test_modbus_connection(connection_dict)
if not result["success"]:
return error_response(message=result["message"], code=400)
return format_response(data=result["data"], message=result["message"])
except Exception as e:
logger.error(f"测试Modbus连接失败: {str(e)}")
return error_response(message=f"测试Modbus连接失败: {str(e)}", code=500)
@router.get("/list")
async def get_modbus_config_list(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, description="每页数量"),
name: Optional[str] = Query(None, description="配置名称"),
ip: Optional[str] = Query(None, description="设备IP地址")
):
"""
获取Modbus配置列表
Args:
page: 页码
size: 每页数量
name: 配置名称用于筛选
ip: 设备IP地址用于筛选
Returns:
包含Modbus配置列表的响应
"""
try:
# 创建查询参数对象
params = ModbusConfigListParams(
pageNum=page,
pageSize=size,
name=name,
ip=ip
)
result = await ModbusConfigService.get_modbus_config_list(
page=params.pageNum,
size=params.pageSize,
name=params.name,
ip=params.ip
)
if not result["success"]:
return error_response(message=result["message"], code=500)
return format_response(data=result["data"], message=result["message"])
except Exception as e:
logger.error(f"获取Modbus配置列表失败: {str(e)}")
return error_response(message=f"获取Modbus配置列表失败: {str(e)}", code=500)
@router.put("/{config_id}")
async def update_modbus_config(
config_id: str = Path(..., description="配置ID"),
config_data: ModbusConfigUpdateRequest = Body(..., description="Modbus配置信息")
):
"""
修改Modbus配置
Args:
config_id: Modbus配置ID
config_data: 修改后的Modbus配置信息
Returns:
包含修改结果的响应
"""
try:
# 转换为字典
config_dict = config_data.model_dump(exclude_unset=True)
result = await ModbusConfigService.update_modbus_config(config_id, config_dict)
if not result["success"]:
return error_response(message=result["message"], code=400)
return format_response(data=result["data"], message=result["message"])
except Exception as e:
logger.error(f"修改Modbus配置失败: {str(e)}")
return error_response(message=f"修改Modbus配置失败: {str(e)}", code=500)
@router.get("/{config_id}")
async def get_modbus_config_detail(
config_id: str = Path(..., description="配置ID")
):
"""
获取指定Modbus配置详情
Args:
config_id: Modbus配置ID
Returns:
包含Modbus配置详情的响应
"""
try:
result = await ModbusConfigService.get_modbus_config_detail(config_id)
if not result["success"]:
return error_response(message=result["message"], code=404)
return format_response(data=result["data"], message=result["message"])
except Exception as e:
logger.error(f"获取Modbus配置详情失败: {str(e)}")
return error_response(message=f"获取Modbus配置详情失败: {str(e)}", code=500)

Binary file not shown.

View File

@ -0,0 +1,52 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
呼叫器设备模型模块
定义呼叫器设备相关的请求和响应模型
"""
from typing import List, Optional
from pydantic import BaseModel, Field
# 定义按钮模型
class CallDeviceButtonModel(BaseModel):
"""呼叫器设备按钮模型"""
signal_name: str = Field(..., description="信号名称", min_length=1, max_length=100)
signal_type: int = Field(..., description="信号类型 1:按钮 2:灯")
signal_length: str = Field(..., description="信号长度")
register_address: str = Field(..., description="寄存器地址")
function_code: Optional[str] = Field(..., description="功能码")
remark: Optional[str] = Field(None, description="备注")
vwed_task_id: Optional[str] = Field(None, description="按下灯亮绑定的VWED任务ID")
long_vwed_task_id: Optional[str] = Field(None, description="长按取消后触发VWED任务ID")
# 定义呼叫器模型
class CallDeviceModel(BaseModel):
"""呼叫器设备模型"""
protocol: str = Field(..., description="协议类型")
brand: str = Field("艾智威", description="品牌", min_length=1, max_length=50)
ip: str = Field(..., description="IP地址")
port: int = Field(..., description="端口号")
device_name: str = Field(..., description="设备名称", min_length=1, max_length=100)
status: int = Field(0, description="状态(0:禁用,1:启用)")
slave_id: Optional[str] = Field("1", description="从机ID")
function_code: Optional[str] = Field("4x", description="功能码")
buttons: List[CallDeviceButtonModel] = Field([], description="呼叫器按钮列表")
# 定义删除设备请求模型
class DeleteDeviceRequest(BaseModel):
ids: List[str]
# 定义导出设备请求模型
class ExportDevicesRequest(BaseModel):
"""导出呼叫器设备请求模型"""
ids: List[str] = Field(..., description="要导出的呼叫器设备ID列表")
# 定义导入设备结果模型
class ImportDeviceResult(BaseModel):
"""导入呼叫器设备结果模型"""
success_count: int = Field(0, description="成功导入数量")
failed_count: int = Field(0, description="导入失败数量")
failed_devices: List[dict] = Field([], description="导入失败的设备信息")
imported_devices: List[dict] = Field([], description="成功导入的设备信息")

155
routes/model/map_model.py Normal file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
地图数据推送相关模型
用于处理地图数据推送时的动作点和库区信息
"""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field, validator
from enum import Enum
class StorageAreaTypeEnum(str, Enum):
"""库区类型枚举"""
GENERAL = "general"
DENSE = "dense"
class OperatePointLayerData(BaseModel):
"""动作点分层数据"""
layer_index: int = Field(..., ge=1, description="层索引(从1开始)")
layer_name: Optional[str] = Field(None, description="层名称")
max_weight: Optional[int] = Field(None, ge=0, description="最大承重(克)")
max_volume: Optional[int] = Field(None, ge=0, description="最大体积(立方厘米)")
layer_height: Optional[int] = Field(None, ge=0, description="层高(毫米)")
description: Optional[str] = Field(None, description="层描述")
tags: Optional[str] = Field(None, description="层标签")
class OperatePointData(BaseModel):
"""动作点数据"""
station_name: str = Field(..., description="动作站点名称")
storage_location_name: str = Field(..., description="库位名称")
storage_area_id: Optional[str] = Field(None, description="所属库区ID")
max_layers: int = Field(1, ge=1, description="最大层数")
position_x: Optional[int] = Field(None, description="X坐标")
position_y: Optional[int] = Field(None, description="Y坐标")
position_z: Optional[int] = Field(None, description="Z坐标")
content: Optional[str] = Field("", description="内容")
tags: Optional[str] = Field("", description="标签")
description: Optional[str] = Field(None, description="动作点描述")
layers: Optional[List[OperatePointLayerData]] = Field(None, description="分层数据")
@validator('layers')
def validate_layers(cls, v, values):
"""验证分层数据"""
if v is None:
return v
max_layers = values.get('max_layers', 1)
if len(v) > max_layers:
raise ValueError(f"分层数量({len(v)})不能超过最大层数({max_layers})")
# 检查层索引是否重复
layer_indices = [layer.layer_index for layer in v]
if len(layer_indices) != len(set(layer_indices)):
raise ValueError("层索引不能重复")
# 检查层索引是否超出范围
for layer in v:
if layer.layer_index > max_layers:
raise ValueError(f"层索引({layer.layer_index})不能超过最大层数({max_layers})")
return v
class StorageAreaData(BaseModel):
"""库区数据"""
id: str = Field(..., description="库区ID")
area_name: str = Field(..., description="库区名称")
area_code: str = Field(..., description="库区编码")
area_type: StorageAreaTypeEnum = Field(StorageAreaTypeEnum.GENERAL, description="库区类型")
description: Optional[str] = Field(None, description="库区描述")
tags: Optional[str] = Field(None, description="库区标签")
class MapDataPushRequest(BaseModel):
"""地图数据推送请求"""
scene_id: str = Field(..., description="场景ID")
storage_areas: List[StorageAreaData] = Field(..., description="库区数据列表")
operate_points: List[OperatePointData] = Field(..., description="动作点数据列表")
@validator('operate_points')
def validate_operate_points(cls, v, values):
"""验证动作点数据"""
if not v:
return v
# 检查站点名称是否重复
station_names = [point.station_name for point in v]
if len(station_names) != len(set(station_names)):
raise ValueError("动作站点名称不能重复")
# 检查库区ID是否存在
storage_areas = values.get('storage_areas', [])
storage_area_ids = {area.id for area in storage_areas}
for point in v:
if point.storage_area_id and point.storage_area_id not in storage_area_ids:
raise ValueError(f"动作点'{point.station_name}'的库区ID'{point.storage_area_id}'不存在")
return v
@validator('storage_areas')
def validate_storage_areas(cls, v):
"""验证库区数据"""
if not v:
return v
# 检查库区ID是否重复
area_ids = [area.id for area in v]
if len(area_ids) != len(set(area_ids)):
raise ValueError("库区ID不能重复")
# 检查库区名称是否重复
area_names = [area.area_name for area in v]
if len(area_names) != len(set(area_names)):
raise ValueError("库区名称不能重复")
# 检查库区编码是否重复
area_codes = [area.area_code for area in v]
if len(area_codes) != len(set(area_codes)):
raise ValueError("库区编码不能重复")
return v
class MapDataPushResponse(BaseModel):
"""地图数据推送响应"""
scene_id: str = Field(..., description="场景ID")
storage_areas_count: int = Field(..., description="创建的库区数量")
operate_points_count: int = Field(..., description="创建的动作点数量")
layers_count: int = Field(..., description="创建的分层数量")
message: str = Field("推送成功,已覆盖现有数据", description="推送结果说明")
class MapDataQueryRequest(BaseModel):
"""地图数据查询请求"""
scene_id: str = Field(..., description="场景ID")
area_type: Optional[StorageAreaTypeEnum] = Field(None, description="库区类型筛选")
include_layers: bool = Field(True, description="是否包含分层数据")
class MapDataQueryResponse(BaseModel):
"""地图数据查询响应"""
scene_id: str = Field(..., description="场景ID")
storage_areas: List[Dict[str, Any]] = Field(..., description="库区数据列表")
operate_points: List[Dict[str, Any]] = Field(..., description="动作点数据列表")
total_capacity: int = Field(0, description="总容量")
used_capacity: int = Field(0, description="已使用容量")
dense_areas_count: int = Field(0, description="密集库区数量")
general_areas_count: int = Field(0, description="一般库区数量")
total_layers: int = Field(0, description="总分层数量")
occupied_layers: int = Field(0, description="已占用分层数量")

View File

@ -0,0 +1,109 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Modbus配置模型模块
包含Modbus配置相关的请求和响应数据模型
"""
from typing import Optional, List, Dict, Any, Union
from enum import Enum
from pydantic import BaseModel, Field, validator
import re
from routes.model.base import PageResult, PaginationParams
# Modbus地址类型枚举
class ModbusAddressType(str, Enum):
"""Modbus地址类型枚举"""
COIL = "0X" # 线圈寄存器
DISCRETE_INPUT = "1X" # 离散输入寄存器
INPUT_REGISTER = "3X" # 输入寄存器
HOLDING_REGISTER = "4X" # 保持寄存器
# Modbus配置添加请求模型
class ModbusConfigAddRequest(BaseModel):
"""Modbus配置添加请求模型"""
name: str = Field(..., description="配置名称", min_length=1, max_length=100)
ip: str = Field(..., description="设备IP地址")
port: int = Field(..., description="通信端口号")
slave_id: int = Field(..., description="从站ID")
address_type: ModbusAddressType = Field(..., description="地址类型")
address_number: int = Field(..., description="地址编号")
task_id: Optional[str] = Field(None, description="任务ID")
target_value: Optional[int] = Field(None, description="目标值")
reset_after_trigger: Optional[bool] = Field(False, description="触发后是否重置")
reset_signal_address: Optional[int] = Field(None, description="重置信号地址")
reset_value: Optional[int] = Field(None, description="重置值")
remark: Optional[str] = Field(None, description="备注")
tenant_id: Optional[str] = Field(None, description="租户ID")
# Modbus配置更新请求模型
class ModbusConfigUpdateRequest(BaseModel):
"""Modbus配置更新请求模型"""
name: Optional[str] = Field(None, description="配置名称", min_length=1, max_length=100)
ip: Optional[str] = Field(None, description="设备IP地址")
port: Optional[int] = Field(None, description="通信端口号")
slave_id: Optional[int] = Field(None, description="从站ID")
address_type: Optional[ModbusAddressType] = Field(None, description="地址类型")
address_number: Optional[int] = Field(None, description="地址编号")
task_id: Optional[str] = Field(None, description="任务ID")
target_value: Optional[int] = Field(None, description="目标值")
remark: Optional[str] = Field(None, description="备注")
# Modbus连接测试请求模型
class ModbusConnectionTestRequest(BaseModel):
"""Modbus连接测试请求模型"""
ip: str = Field(..., description="设备IP地址")
port: int = Field(..., description="通信端口号")
slave_id: int = Field(..., description="从站ID")
address_type: Optional[ModbusAddressType] = Field(None, description="地址类型")
address_number: Optional[int] = Field(None, description="地址编号")
# Modbus配置详情返回模型
class ModbusConfigDetail(BaseModel):
"""Modbus配置详情返回模型"""
id: str = Field(..., description="配置ID")
name: str = Field(..., description="配置名称", min_length=1, max_length=100)
ip: str = Field(..., description="设备IP地址")
port: int = Field(..., description="通信端口号")
slave_id: int = Field(..., description="从站ID")
address_type: str = Field(..., description="地址类型")
address_number: int = Field(..., description="地址编号")
task_id: Optional[str] = Field(None, description="任务ID")
target_value: Optional[int] = Field(None, description="目标值")
reset_after_trigger: Optional[bool] = Field(False, description="触发后是否重置")
reset_signal_address: Optional[int] = Field(None, description="重置信号地址")
reset_value: Optional[int] = Field(None, description="重置值")
remark: Optional[str] = Field(None, description="备注")
status: int = Field(..., description="状态1:启用, 0:禁用)")
tenant_id: Optional[str] = Field(None, description="租户ID")
create_date: Optional[str] = Field(None, description="创建时间")
update_date: Optional[str] = Field(None, description="更新时间")
# Modbus配置列表查询参数
class ModbusConfigListParams(PaginationParams):
"""Modbus配置列表查询参数"""
name: Optional[str] = Field(None, description="配置名称,用于筛选", min_length=1, max_length=100)
ip: Optional[str] = Field(None, description="设备IP地址用于筛选")
# Modbus配置列表项模型
class ModbusConfigListItem(BaseModel):
"""Modbus配置列表项模型"""
id: str = Field(..., description="配置ID")
name: str = Field(..., description="配置名称", min_length=1, max_length=100)
ip: str = Field(..., description="设备IP地址")
port: int = Field(..., description="通信端口号")
slave_id: int = Field(..., description="从站ID")
address_type: str = Field(..., description="地址类型")
address_number: int = Field(..., description="地址编号")
target_value: Optional[int] = Field(None, description="目标值")
remark: Optional[str] = Field(None, description="备注")
status: int = Field(..., description="状态1:启用, 0:禁用)")
create_date: Optional[str] = Field(None, description="创建时间")
# Modbus配置列表响应模型
class ModbusConfigListResponse(PageResult):
"""Modbus配置列表响应模型"""
records: List[ModbusConfigListItem] = Field(..., description="配置列表")

View File

@ -0,0 +1,320 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
动作点管理相关模型
用于处理动作点和库位的管理操作
"""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
from enum import Enum
from datetime import datetime
from data.models.extended_property import ExtendedPropertyTypeEnum
class StorageAreaTypeEnum(str, Enum):
"""库区类型枚举"""
GENERAL = "general"
DENSE = "dense"
class StorageLocationActionEnum(str, Enum):
"""库位操作类型枚举"""
OCCUPY = "occupy" # 占用库位
RELEASE = "release" # 释放库位
LOCK = "lock" # 锁定库位
UNLOCK = "unlock" # 解锁库位
ENABLE = "enable" # 启用库位
DISABLE = "disable" # 禁用库位
SET_EMPTY_TRAY = "set_empty_tray" # 设置为空托盘
CLEAR_EMPTY_TRAY = "clear_empty_tray" # 清除空托盘状态
class OperatePointLayerInfo(BaseModel):
"""动作点分层信息"""
id: str = Field(..., description="层ID")
layer_index: int = Field(..., description="层索引")
layer_name: Optional[str] = Field(None, description="层名称")
is_occupied: bool = Field(..., description="是否占用")
is_locked: bool = Field(..., description="是否锁定")
is_disabled: bool = Field(..., description="是否禁用")
is_empty_tray: bool = Field(..., description="是否空托盘")
goods_content: str = Field(..., description="货物内容")
goods_weight: Optional[int] = Field(None, description="货物重量(克)")
goods_volume: Optional[int] = Field(None, description="货物体积(立方厘米)")
max_weight: Optional[int] = Field(None, description="最大承重(克)")
max_volume: Optional[int] = Field(None, description="最大体积(立方厘米)")
layer_height: Optional[int] = Field(None, description="层高(毫米)")
locked_by: Optional[str] = Field(None, description="锁定者")
tags: Optional[str] = Field(None, description="标签")
description: Optional[str] = Field(None, description="层描述")
goods_stored_at: Optional[datetime] = Field(None, description="货物存放时间")
goods_retrieved_at: Optional[datetime] = Field(None, description="货物取出时间")
last_access_at: Optional[datetime] = Field(None, description="最后访问时间")
# 库位列表相关模型
class StorageLocationInfo(BaseModel):
"""库位信息(基于动作点分层)"""
# 库位基本信息
id: str = Field(..., description="库位ID层ID")
layer_index: int = Field(..., description="层索引")
layer_name: Optional[str] = Field(None, description="层名称")
# 库位状态
is_occupied: bool = Field(..., description="是否占用")
is_locked: bool = Field(..., description="是否锁定")
is_disabled: bool = Field(..., description="是否禁用")
is_empty_tray: bool = Field(..., description="是否空托盘")
locked_by: Optional[str] = Field(None, description="锁定者")
# 货物信息
goods_content: str = Field(..., description="货物内容")
goods_weight: Optional[int] = Field(None, description="货物重量(克)")
goods_volume: Optional[int] = Field(None, description="货物体积(立方厘米)")
goods_stored_at: Optional[datetime] = Field(None, description="货物存放时间")
goods_retrieved_at: Optional[datetime] = Field(None, description="货物取出时间")
last_access_at: Optional[datetime] = Field(None, description="最后访问时间")
# 库位规格
max_weight: Optional[int] = Field(None, description="最大承重(克)")
max_volume: Optional[int] = Field(None, description="最大体积(立方厘米)")
layer_height: Optional[int] = Field(None, description="层高(毫米)")
# 扩展信息
tags: Optional[str] = Field(None, description="标签")
description: Optional[str] = Field(None, description="库位描述")
created_at: Optional[datetime] = Field(None, description="创建时间")
updated_at: Optional[datetime] = Field(None, description="更新时间")
# 扩展字段
extended_fields: Optional[Dict[str, Any]] = Field(None, description="扩展字段")
# 动作点信息(直接平铺)
operate_point_id: Optional[str] = Field(None, description="所属动作点ID")
station_name: Optional[str] = Field(None, description="动作站点名称")
storage_location_name: Optional[str] = Field(None, description="库位名称")
scene_id: Optional[str] = Field(None, description="场景ID")
storage_area_id: Optional[str] = Field(None, description="所属库区ID")
storage_area_type: Optional[str] = Field(None, description="库区类型")
area_name: Optional[str] = Field(None, description="库区名称")
max_layers: Optional[int] = Field(None, description="最大层数")
current_layers: Optional[int] = Field(None, description="当前使用层数")
position_x: Optional[int] = Field(None, description="X坐标")
position_y: Optional[int] = Field(None, description="Y坐标")
position_z: Optional[int] = Field(None, description="Z坐标")
operate_point_description: Optional[str] = Field(None, description="动作点描述")
class StorageLocationListRequest(BaseModel):
"""库位列表查询请求"""
scene_id: Optional[str] = Field(None, description="场景ID")
storage_area_id: Optional[str] = Field(None, description="库区ID")
station_name: Optional[str] = Field(None, description="站点名称(支持模糊搜索)")
storage_location_name: Optional[str] = Field(None, description="库位名称(支持模糊搜索)")
layer_name: Optional[str] = Field(None, description="层名称(支持模糊搜索)")
is_disabled: Optional[bool] = Field(None, description="是否禁用")
is_occupied: Optional[bool] = Field(None, description="是否占用")
is_locked: Optional[bool] = Field(None, description="是否锁定")
is_empty_tray: Optional[bool] = Field(None, description="是否空托盘")
include_operate_point_info: bool = Field(True, description="是否包含动作点信息")
include_extended_fields: bool = Field(True, description="是否包含扩展字段")
page: int = Field(1, ge=1, description="页码")
page_size: int = Field(20, ge=1, le=100, description="每页数量")
class StorageLocationListResponse(BaseModel):
"""库位列表查询响应"""
total: int = Field(..., description="总数量")
page: int = Field(..., description="当前页码")
page_size: int = Field(..., description="每页数量")
total_pages: int = Field(..., description="总页数")
storage_locations: List[StorageLocationInfo] = Field(..., description="库位列表")
class StorageLocationStatistics(BaseModel):
"""库位统计信息"""
total_storage_locations: int = Field(..., description="总库位数")
occupied_storage_locations: int = Field(..., description="已占用库位数")
available_storage_locations: int = Field(..., description="可用库位数")
disabled_storage_locations: int = Field(..., description="禁用库位数")
locked_storage_locations: int = Field(..., description="锁定库位数")
empty_tray_storage_locations: int = Field(..., description="空托盘库位数")
dense_area_storage_locations: int = Field(..., description="密集库区库位数")
general_area_storage_locations: int = Field(..., description="一般库区库位数")
occupancy_rate: float = Field(..., description="占用率")
availability_rate: float = Field(..., description="可用率")
# 库位状态管理相关模型
class StorageLocationStatusUpdateRequest(BaseModel):
"""库位状态更新请求"""
storage_location_id: str = Field(..., description="库位ID层ID")
action: StorageLocationActionEnum = Field(..., description="操作类型")
locked_by: Optional[str] = Field(None, description="锁定者(锁定操作时必填)")
reason: Optional[str] = Field(None, description="操作原因")
class StorageLocationStatusUpdateResponse(BaseModel):
"""库位状态更新响应"""
storage_location_id: str = Field(..., description="库位ID")
action: StorageLocationActionEnum = Field(..., description="执行的操作")
success: bool = Field(..., description="操作是否成功")
message: str = Field(..., description="操作结果消息")
new_status: Dict[str, Any] = Field(..., description="更新后的状态信息")
class BatchStorageLocationStatusUpdateRequest(BaseModel):
"""批量库位状态更新请求"""
storage_location_ids: List[str] = Field(..., description="库位ID列表")
action: StorageLocationActionEnum = Field(..., description="操作类型")
locked_by: Optional[str] = Field(None, description="锁定者(锁定操作时必填)")
reason: Optional[str] = Field(None, description="操作原因")
class BatchStorageLocationStatusUpdateResponse(BaseModel):
"""批量库位状态更新响应"""
total_count: int = Field(..., description="总操作数量")
success_count: int = Field(..., description="成功操作数量")
failed_count: int = Field(..., description="失败操作数量")
results: List[StorageLocationStatusUpdateResponse] = Field(..., description="详细操作结果列表")
# 扩展属性管理相关模型
class ExtendedPropertyCreateRequest(BaseModel):
"""扩展属性创建请求"""
property_name: str = Field(..., description="属性名称(唯一标识)", min_length=1, max_length=200)
property_type: ExtendedPropertyTypeEnum = Field(ExtendedPropertyTypeEnum.STRING, description="属性类型")
is_required: bool = Field(False, description="是否必填")
is_enabled: bool = Field(True, description="是否启用")
description: Optional[str] = Field(None, description="属性描述")
placeholder: Optional[str] = Field(None, description="输入提示", max_length=500)
default_value: Optional[str] = Field(None, description="默认值")
options: Optional[List[Dict[str, Any]]] = Field(None, description="选项值用于select和multiselect类型")
validation_rules: Optional[Dict[str, Any]] = Field(None, description="验证规则")
category: Optional[str] = Field(None, description="属性分类", max_length=100)
sort_order: int = Field(0, description="排序顺序")
display_width: int = Field(200, description="显示宽度(像素)")
display_format: Optional[str] = Field(None, description="显示格式", max_length=100)
class ExtendedPropertyInfo(BaseModel):
"""扩展属性信息"""
id: str = Field(..., description="属性ID")
property_name: str = Field(..., description="属性名称(唯一标识)")
property_type: ExtendedPropertyTypeEnum = Field(..., description="属性类型")
is_required: bool = Field(..., description="是否必填")
is_enabled: bool = Field(..., description="是否启用")
description: Optional[str] = Field(None, description="属性描述")
placeholder: Optional[str] = Field(None, description="输入提示")
default_value: Optional[str] = Field(None, description="默认值")
options: Optional[List[Dict[str, Any]]] = Field(None, description="选项值")
validation_rules: Optional[Dict[str, Any]] = Field(None, description="验证规则")
category: Optional[str] = Field(None, description="属性分类")
sort_order: int = Field(..., description="排序顺序")
display_width: int = Field(..., description="显示宽度(像素)")
display_format: Optional[str] = Field(None, description="显示格式")
created_at: datetime = Field(..., description="创建时间")
updated_at: datetime = Field(..., description="更新时间")
class ExtendedPropertyCreateResponse(BaseModel):
"""扩展属性创建响应"""
id: str = Field(..., description="创建的属性ID")
property_name: str = Field(..., description="属性名称(唯一标识)")
message: str = Field(..., description="创建结果消息")
class ExtendedPropertyListRequest(BaseModel):
"""扩展属性列表查询请求"""
property_name: Optional[str] = Field(None, description="属性名称(支持模糊搜索)")
property_type: Optional[ExtendedPropertyTypeEnum] = Field(None, description="属性类型")
category: Optional[str] = Field(None, description="属性分类")
is_enabled: Optional[bool] = Field(None, description="是否启用")
page: int = Field(1, ge=1, description="页码")
page_size: int = Field(20, ge=1, le=100, description="每页数量")
class ExtendedPropertyListResponse(BaseModel):
"""扩展属性列表响应"""
total: int = Field(..., description="总数量")
page: int = Field(..., description="当前页码")
page_size: int = Field(..., description="每页数量")
total_pages: int = Field(..., description="总页数")
properties: List[ExtendedPropertyInfo] = Field(..., description="扩展属性列表")
class ExtendedPropertyDeleteResponse(BaseModel):
"""扩展属性删除响应"""
property_id: str = Field(..., description="删除的属性ID")
property_name: str = Field(..., description="属性名称(唯一标识)")
message: str = Field(..., description="删除结果消息")
# 库位详情相关模型
class StorageLocationDetailResponse(BaseModel):
"""库位详情响应"""
storage_location: StorageLocationInfo = Field(..., description="库位详细信息")
operate_point_info: Optional[Dict[str, Any]] = Field(None, description="动作点详细信息")
extended_fields_definitions: Optional[List[ExtendedPropertyInfo]] = Field(None, description="扩展字段定义")
status_history: Optional[List[Dict[str, Any]]] = Field(None, description="状态变更历史")
# 库位编辑相关模型
class StorageLocationEditRequest(BaseModel):
"""库位编辑请求"""
goods_content: Optional[str] = Field(None, description="货物内容", max_length=500)
goods_weight: Optional[int] = Field(None, description="货物重量(克)", ge=0)
goods_volume: Optional[int] = Field(None, description="货物体积(立方厘米)", ge=0)
max_weight: Optional[int] = Field(None, description="最大承重(克)", ge=0)
max_volume: Optional[int] = Field(None, description="最大体积(立方厘米)", ge=0)
layer_height: Optional[int] = Field(None, description="层高(毫米)", ge=0)
is_locked: Optional[bool] = Field(None, description="是否锁定")
is_disabled: Optional[bool] = Field(None, description="是否禁用")
is_empty_tray: Optional[bool] = Field(None, description="是否空托盘")
tags: Optional[str] = Field(None, description="标签", max_length=500)
description: Optional[str] = Field(None, description="库位描述", max_length=1000)
extended_fields: Optional[Dict[str, Any]] = Field(None, description="扩展字段值")
class StorageLocationEditResponse(BaseModel):
"""库位编辑响应"""
storage_location_id: str = Field(..., description="库位ID")
success: bool = Field(..., description="编辑是否成功")
message: str = Field(..., description="操作结果消息")
updated_fields: List[str] = Field(..., description="已更新的字段列表")
updated_storage_location: StorageLocationInfo = Field(..., description="更新后的库位信息")
# 库位操作记录相关模型
class StorageLocationLogInfo(BaseModel):
"""库位操作记录信息"""
id: str = Field(..., description="记录ID")
operation_time: datetime = Field(..., description="操作时间")
operator: str = Field(..., description="操作人")
operation_type: str = Field(..., description="操作类型")
affected_storage_locations: List[str] = Field(..., description="影响的库位ID列表")
description: Optional[str] = Field(None, description="操作描述")
created_at: datetime = Field(..., description="创建时间")
class StorageLocationLogListRequest(BaseModel):
"""库位操作记录查询请求"""
storage_location_id: Optional[str] = Field(None, description="库位ID")
operator: Optional[str] = Field(None, description="操作人(支持模糊搜索)")
operation_type: Optional[str] = Field(None, description="操作类型")
start_time: Optional[datetime] = Field(None, description="开始时间")
end_time: Optional[datetime] = Field(None, description="结束时间")
page: int = Field(1, ge=1, description="页码")
page_size: int = Field(20, ge=1, le=100, description="每页数量")
class StorageLocationLogListResponse(BaseModel):
"""库位操作记录查询响应"""
total: int = Field(..., description="总数量")
page: int = Field(..., description="当前页码")
page_size: int = Field(..., description="每页数量")
total_pages: int = Field(..., description="总页数")
logs: List[StorageLocationLogInfo] = Field(..., description="操作记录列表")

View File

@ -15,9 +15,9 @@ class ScriptListParams(BaseModel):
"""脚本列表查询参数"""
page: int = Field(1, description="页码默认为1")
pageSize: int = Field(20, description="每页记录数默认为20")
name: Optional[str] = Field(None, description="脚本名称,支持模糊查询")
name: Optional[str] = Field(None, description="脚本名称,支持模糊查询", min_length=1, max_length=100)
status: Optional[int] = Field(None, description="脚本状态(1:启用, 0:禁用)")
folderPath: Optional[str] = Field(None, description="脚本所在目录路径")
folderPath: Optional[str] = Field(None, description="脚本所在目录路径", max_length=500)
tags: Optional[str] = Field(None, description="标签,支持模糊查询")
isPublic: Optional[int] = Field(None, description="是否公开(1:是, 0:否)")
createdBy: Optional[str] = Field(None, description="创建者")
@ -28,10 +28,10 @@ class ScriptListParams(BaseModel):
class CreateScriptRequest(BaseModel):
"""创建脚本请求"""
folderPath: Optional[str] = Field("/", description="脚本所在目录路径,默认为根目录")
fileName: str = Field(..., description="脚本文件名,必须以.py结尾")
folderPath: Optional[str] = Field("/", description="脚本所在目录路径,默认为根目录", max_length=500)
fileName: str = Field(..., description="脚本文件名,必须以.py结尾", min_length=1, max_length=255)
code: str = Field(..., description="脚本代码内容")
name: Optional[str] = Field(None, description="脚本名称,默认使用文件名(不含扩展名)")
name: Optional[str] = Field(None, description="脚本名称,默认使用文件名(不含扩展名)", min_length=1, max_length=100)
description: Optional[str] = Field(None, description="脚本功能描述")
status: Optional[int] = Field(1, description="状态(1:启用, 0:禁用)")
isPublic: Optional[int] = Field(1, description="是否公开(1:是, 0:否)")
@ -40,10 +40,10 @@ class CreateScriptRequest(BaseModel):
class UpdateScriptRequest(BaseModel):
"""更新脚本请求"""
folderPath: Optional[str] = Field(None, description="脚本所在目录路径")
fileName: Optional[str] = Field(None, description="脚本文件名,必须以.py结尾")
folderPath: Optional[str] = Field(None, description="脚本所在目录路径", max_length=500)
fileName: Optional[str] = Field(None, description="脚本文件名,必须以.py结尾", min_length=1, max_length=255)
code: Optional[str] = Field(None, description="脚本代码内容")
name: Optional[str] = Field(None, description="脚本名称")
name: Optional[str] = Field(None, description="脚本名称", min_length=1, max_length=100)
description: Optional[str] = Field(None, description="脚本功能描述")
status: Optional[int] = Field(None, description="状态(1:启用, 0:禁用)")
isPublic: Optional[int] = Field(None, description="是否公开(1:是, 0:否)")
@ -69,9 +69,9 @@ class RestoreScriptVersionRequest(BaseModel):
class ScriptInfo(BaseModel):
"""脚本信息"""
id: str = Field(..., description="脚本ID")
name: str = Field(..., description="脚本名称")
folderPath: str = Field(..., description="脚本所在目录路径")
fileName: str = Field(..., description="脚本文件名")
name: str = Field(..., description="脚本名称", min_length=1, max_length=100)
folderPath: str = Field(..., description="脚本所在目录路径", max_length=500)
fileName: str = Field(..., description="脚本文件名", min_length=1, max_length=255)
description: Optional[str] = Field(None, description="脚本功能描述")
version: int = Field(..., description="当前版本号")
status: int = Field(..., description="状态(1:启用, 0:禁用)")
@ -122,8 +122,8 @@ class ScriptLogDetail(ScriptLogInfo):
class FolderNode(BaseModel):
"""脚本目录节点"""
name: str = Field(..., description="目录名称")
path: str = Field(..., description="目录路径")
name: str = Field(..., description="目录名称", min_length=1, max_length=100)
path: str = Field(..., description="目录路径", max_length=500)
children: List["FolderNode"] = Field(default_factory=list, description="子目录")
class ScriptCreateResponse(BaseModel):

View File

@ -32,9 +32,9 @@ class ParamValueType(str, Enum):
# 任务参数模型
class TaskParam(BaseModel):
"""任务参数模型"""
name: str = Field(..., description="参数名称")
name: str = Field(..., description="参数名称", min_length=1, max_length=50)
type: str = Field(..., description="参数类型")
label: str = Field(..., description="参数显示名称")
label: str = Field(..., description="参数显示名称", min_length=1, max_length=100)
required: bool = Field(False, description="是否必填")
defaultValue: Optional[Any] = Field(None, description="默认值")
remark: Optional[str] = Field(None, description="备注说明")
@ -52,7 +52,7 @@ class ParamValue(BaseModel):
class BlockBase(BaseModel):
"""块基本模型"""
id: int = Field(..., description="块ID")
name: str = Field(..., description="块名称")
name: str = Field(..., description="块名称", min_length=1, max_length=100)
blockType: str = Field(..., description="块类型")
inputParams: Dict[str, ParamValue] = Field(default={}, description="输入参数")
children: Dict[str, List[Any]] = Field(default={}, description="子块列表")
@ -113,7 +113,7 @@ class TaskBasicInfo(BaseModel):
# 任务备份请求模型
class TaskBackupRequest(BaseModel):
"""任务备份请求模型"""
backup_name: Optional[str] = Field(None, description="备份后的任务名称")
backupName: Optional[str] = Field(None, description="备份后的任务名称")
remark: Optional[str] = Field(None, description="备份任务的备注信息")
# 子任务列表查询参数
@ -171,22 +171,29 @@ class InputParamType(str, Enum):
class TaskInputParam(BaseModel):
"""任务输入参数模型"""
name: str = Field(..., description="参数名称")
name: str = Field(..., description="参数名称", min_length=1, max_length=50)
type: InputParamType = Field(..., description="参数类型")
label: str = Field(..., description="参数显示名称")
label: str = Field(..., description="参数显示名称", min_length=1, max_length=100)
required: bool = Field(False, description="是否必填")
defaultValue: str = Field("", description="默认值")
remark: str = Field("", description="参数说明")
class TaskInputParamNew(BaseModel):
"""任务输入参数模型"""
name: str = Field(..., description="参数名称")
name: str = Field(..., description="参数名称", min_length=1, max_length=50)
type: InputParamType = Field(..., description="参数类型")
label: str = Field(..., description="参数显示名称")
label: str = Field(..., description="参数显示名称", min_length=1, max_length=100)
required: bool = Field(False, description="是否必填")
defaultValue: str = Field(..., description="默认值")
remark: str = Field("", description="参数说明")
# Modbus配置参数模型
class ModbusConfigParam(BaseModel):
"""Modbus配置参数模型"""
config_id: str = Field(..., description="Modbus配置ID")
name: Optional[str] = Field(None, description="配置名称,用于显示")
operation: str = Field("read", description="操作类型read或write")
# 任务运行请求模型
class TaskEditRunRequest(BaseModel):
"""任务运行请求模型"""
@ -195,14 +202,17 @@ class TaskEditRunRequest(BaseModel):
source_type: Optional[int] = Field(..., description="任务来源类型1: 系统调度, 2: 呼叫机, 3: 第三方系统, 4: 手持电脑)")
source_system: Optional[str] = Field(..., description="来源系统标识WMS、MES等系统编号")
source_device: Optional[str] = Field(..., description="下达任务的硬件设备标识设备ID、MAC地址等")
modbus_configs: Optional[List[ModbusConfigParam]] = Field(None, description="任务关联的Modbus配置列表用于任务执行时的Modbus操作")
use_modbus: Optional[bool] = Field(False, description="是否使用Modbus通信")
modbus_timeout: Optional[int] = Field(5000, description="Modbus通信超时时间(毫秒)")
# 重新定义更准确的任务参数模型,确保包含所有必要字段
class TaskParamSave(BaseModel):
"""任务参数保存模型"""
name: str = Field(..., description="参数名称")
name: str = Field(..., description="参数名称", min_length=1, max_length=50)
type: str = Field(..., description="参数类型")
label: str = Field(..., description="参数显示名称")
label: str = Field(..., description="参数显示名称", min_length=1, max_length=100)
remark: str = Field("", description="备注说明")
defaultValue: str = Field("", description="默认值")
required: bool = Field(False, description="是否必填")
@ -218,7 +228,7 @@ class BlockParamValue(BaseModel):
class BlockModel(BaseModel):
"""块模型,可以递归包含子块"""
id: int = Field(..., description="块ID")
name: str = Field(..., description="块名称")
name: str = Field(..., description="块名称", min_length=1, max_length=100)
blockType: str = Field(..., description="块类型")
inputParams: Dict[str, BlockParamValue] = Field(default_factory=dict, description="输入参数")
children: Dict[str, List['BlockModel']] = Field(default_factory=dict, description="子块列表")

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