VWED_server/services/sync_service.py
2025-04-30 16:57:46 +08:00

711 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
同步服务模块
用于与天风系统进行数据同步
"""
import json
import aiohttp
import asyncio
from typing import Dict, Any, Optional
from datetime import datetime
from pydantic import BaseModel
# 获取日志记录器
from utils.logger import get_logger
logger = get_logger("services.sync_service")
# 导入天风API配置
from config.tf_api_config import get_tf_api_config
# 获取配置
tf_config = get_tf_api_config()
# 接口配置
class TFApiConfig:
"""系统任务API配置"""
# 基础URL
BASE_URL = tf_config["base_url"]
# 接口路径
CREATE_TASK_PATH = tf_config["endpoints"]["create_task"]
CHOOSE_AMR_PATH = tf_config["endpoints"]["choose_amr"]
ADD_ACTION_PATH = tf_config["endpoints"]["add_action"]
CLOSURE_TASK_PATH = tf_config["endpoints"]["closure_task"]
GET_TASK_BLOCK_PATH = tf_config["endpoints"]["get_task_block"]
GET_TASK_BLOCK_ACTION_PATH = tf_config["endpoints"]["get_task_block_action"]
SET_TASK_IN_PROGRESS_PATH = tf_config["endpoints"]["set_task_in_progress"]
SET_TASK_COMPLETED_PATH = tf_config["endpoints"]["set_task_completed"]
SET_TASK_TERMINATED_PATH = tf_config["endpoints"]["set_task_terminated"]
SET_TASK_FAILED_PATH = tf_config["endpoints"]["set_task_failed"]
# 超时设置(秒)
TIMEOUT = tf_config["timeout"]
# 重试次数
RETRY_TIMES = tf_config["retry_times"]
# 模拟模式
MOCK_MODE = tf_config["mock_mode"]
# token请求头
TOKEN_HEADER = tf_config["token_header"]
class CreateTaskRequest(BaseModel):
"""创建任务请求参数"""
vwedTaskId: str
vwedTaskParentId: str
name: str
isPeriodic: int # 使用字符串类型,因为接口文档中显示为字符串
priority: int # 使用字符串类型,因为接口文档中显示为字符串
createTime: str # 格式: "2025-04-08 22:03:57"
sceneId: str
needAmr: int
class ChooseAmrRequest(BaseModel):
"""选择AMR请求参数"""
vwedTaskId: str # 任务id
stationName: str # 关键站点名称
priority: int = 1 # 优先级
appointAmrName: str = "" # 指定AMR名称
appointAmrGroupName: str = "" # 指定AMR分组名称
class AddActionRequest(BaseModel):
"""添加动作请求参数"""
taskBlockId: str
stationName: str
action: str
class ApiResponse(BaseModel):
"""API响应基础模型"""
success: bool
message: str
code: int
result: Optional[Dict[str, Any]] = None
timestamp: Optional[int] = None
async def create_task(task_record_id: str, task_name: str, is_periodic: bool, priority: int, parent_id: str,
token: str, map_id: str, is_agv: int) -> Optional[ApiResponse]:
"""
调用系统任务创建任务接口
Args:
task_record_id: VWED系统任务实例ID
task_name: 任务名称
is_periodic: 是否周期任务
priority: 优先级(1-5)
parent_id: 父任务ID如果没有则为空
token: 认证令牌
map_id: 相关地图ID
is_agv: 是否选择AGV
Returns:
Optional[ApiResponse]: 接口响应如果请求失败则返回None
"""
# 构造请求参数
request_data = CreateTaskRequest(
vwedTaskId=task_record_id,
vwedTaskParentId=parent_id,
name=task_name,
isPeriodic=int(is_periodic),
priority=str(priority),
createTime=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
sceneId=map_id,
needAmr=is_agv
)
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.CREATE_TASK_PATH}"
# 构建请求头
headers = {}
if token:
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
logger.debug(f"使用认证令牌调用接口,令牌头: {TFApiConfig.TOKEN_HEADER}")
else:
# headers["Authorization"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDU3ODgwMzksInVzZXJuYW1lIjoiYWRtaW4ifQ.QmFrhe9nq8jdNRVtZYo-QK-31hS7AMAwMjwy8EGERQQ"
headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY"
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在同步创建任务到天风系统: {task_record_id}")
logger.debug(f"创建任务请求参数: {request_data.model_dump_json()}")
async with aiohttp.ClientSession() as session:
async with session.post(
url,
json=request_data.model_dump(),
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.text()
# 尝试解析JSON
try:
response_data = json.loads(response_text)
if response_data.get("success"):
logger.info(f"成功同步任务到系统任务: {task_record_id}")
else:
logger.warning(f"同步任务到系统任务失败: {response_data.get('message')}")
return response_data
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except aiohttp.ClientError as e:
logger.error(f"调用系统任务创建任务接口失败: {str(e)}")
return None
except Exception as e:
logger.error(f"同步任务到系统任务时发生错误: {str(e)}")
return None
async def create_choose_amr_task(task_id: str, key_station_name: str, amr_name: str = "", amr_group_name: str = "", token: str = None, priority: int = 1) -> Optional[ApiResponse]:
"""
创建选择AMR任务
Args:
task_id: 天风任务ID
key_station_name: 关键站点名称
amr_name: 指定的AMR名称可选
amr_group_name: 指定的AMR分组名称可选
token: 认证令牌
priority: 优先级
Returns:
Optional[ApiResponse]: 接口响应如果请求失败则返回None
"""
# 构造请求参数
request_data = ChooseAmrRequest(
vwedTaskId=task_id,
stationName=key_station_name,
appointAmrName=amr_name,
appointAmrGroupName=amr_group_name,
priority=priority
)
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.CHOOSE_AMR_PATH}"
# 构建请求头
headers = {}
if token:
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
else:
headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY"
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在创建选择AMR任务: {task_id}, 站点: {key_station_name}")
async with aiohttp.ClientSession() as session:
async with session.post(
url,
json=request_data.model_dump(),
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.json()
# 尝试解析JSON
try:
if response_text.get("success", False):
logger.info(f"成功为任务选择AMR: {task_id}, AMR: {response_text.get('result', {}).get('amrName')}")
else:
logger.warning(f"为任务选择AMR失败: {response_text.get('message', '未知错误')}")
return response_text
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用选择AMR接口失败: {str(e)}")
return None
async def add_action(task_id: str, station_name: str, action: str, token: str = None) -> Optional[ApiResponse]:
"""
调用系统任务添加动作接口
Args:
task_id: 系统任务ID
station_name: 站点名称
action: 动作
token: 认证令牌
Returns:
Optional[ApiResponse]: 接口响应如果请求失败则返回None
"""
# 构造请求参数
request_data = AddActionRequest(
taskBlockId=task_id,
stationName=station_name,
action=action
)
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.ADD_ACTION_PATH}"
# 构建请求头
headers = {}
if token:
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
else:
headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY"
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在为任务添加动作: {task_id}, 站点: {station_name}, 动作: {action}")
async with aiohttp.ClientSession() as session:
async with session.post(
url,
json=request_data.model_dump(),
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.text()
# 尝试解析JSON
try:
response_data = json.loads(response_text)
if response_data.get("success", False):
logger.info(f"成功为任务添加动作: {task_id}, 站点: {station_name}, 动作: {action}")
else:
logger.warning(f"为任务添加动作失败: {response_data.get('message', '未知错误')}")
return response_data
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用添加动作接口失败: {str(e)}")
return None
async def closure_task(task_id: str, token: str = None) -> Optional[ApiResponse]:
"""
调用系统任务封口任务接口
Args:
task_id: 系统任务ID
token: 认证令牌
Returns:
Optional[ApiResponse]: 接口响应如果请求失败则返回None
"""
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.CLOSURE_TASK_PATH.format(task_id)}"
# 构建请求头
headers = {}
if token:
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
else:
headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY"
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在封口任务: {task_id}")
async with aiohttp.ClientSession() as session:
async with session.put(
url,
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.json()
# 尝试解析JSON
try:
if response_text.get("success", False):
logger.info(f"成功封口任务: {task_id}")
else:
logger.warning(f"封口任务失败: {response_text.get('message', '未知错误')}")
return response_text
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用封口任务接口失败: {str(e)}")
return None
async def get_task_block_detail(task_block_id: str, token: str = None) -> Optional[Dict[str, Any]]:
"""
获取任务块详情
Args:
task_block_id: 任务块ID
token: 认证令牌
Returns:
Optional[Dict[str, Any]]: 接口响应如果请求失败则返回None
"""
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.GET_TASK_BLOCK_PATH.format(id=task_block_id)}"
# 构建请求头
headers = {}
if token:
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
else:
headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY"
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在获取任务块详情: {task_block_id}")
async with aiohttp.ClientSession() as session:
async with session.get(
url,
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.text()
# 尝试解析JSON
try:
response_data = json.loads(response_text)
if response_data.get("success", False):
logger.info(f"成功获取任务块详情: {task_block_id} 具体详情: {response_data}")
else:
logger.warning(f"获取任务块详情失败: {response_data.get('message', '未知错误')}")
return response_data
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用获取任务块详情接口失败: {str(e)}")
return None
async def get_task_block_action_detail(task_block_id: str, token: str = None) -> Optional[Dict[str, Any]]:
"""
获取任务块动作详情
Args:
task_block_id: 任务块ID
token: 认证令牌
Returns:
Optional[Dict[str, Any]]: 接口响应如果请求失败则返回None
"""
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.GET_TASK_BLOCK_ACTION_PATH.format(id=task_block_id)}"
# 构建请求头
headers = {}
if token:
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
else:
headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY"
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在获取任务块动作详情: {task_block_id}")
async with aiohttp.ClientSession() as session:
async with session.get(
url,
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.text()
# 尝试解析JSON
try:
response_data = json.loads(response_text)
if response_data.get("success", False):
logger.info(f"成功获取任务块动作详情: {task_block_id} 具体详情: {response_data}")
else:
logger.warning(f"获取任务块动作详情失败: {response_data.get('message', '未知错误')}")
return response_data
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用获取任务块动作详情接口失败: {str(e)}")
return None
async def wait_for_amr_selection(task_block_id: str, token: str = None) -> Optional[Dict[str, Any]]:
"""
等待AMR选择完成轮询任务块详情直到获取到AMR ID
Args:
task_block_id: 任务块ID
token: 认证令牌
Returns:
Optional[Dict[str, Any]]: 包含AMR ID的响应如果请求失败则返回None
"""
logger.info(f"开始等待任务块 {task_block_id} 的AMR选择结果")
retry_count = 0
# 使用固定的重试间隔0.5秒
actual_retry_interval = 0.5
# 无限循环,直到获取到结果
while True:
retry_count += 1
response = await get_task_block_detail(task_block_id, token)
if not response or not response.get("success", False):
logger.warning(f"获取任务块详情失败,继续重试,当前尝试次数: {retry_count}")
await asyncio.sleep(actual_retry_interval)
continue
# 从响应中获取关键字段
result = response.get("result", {})
amr_id = result.get("amrId", "")
# 符合以下条件之一就可以返回:
# 1. amrId有值 - 表示已经分配了AMR
# 2. appointAmrId有值 - 表示指定了特定AMR
# 3. appointAmrGroupId有值 - 表示指定了AMR组
if amr_id:
if amr_id:
logger.info(f"任务块 {task_block_id} 已选择AMR: {amr_id},共尝试 {retry_count}")
return response
# 否则继续等待
if retry_count % 10 == 0:
# 每10次请求记录一次INFO级别日志
logger.info(f"任务块 {task_block_id} AMR选择未完成(amrId/appointAmrId/appointAmrGroupId均为空),已尝试 {retry_count} 次,继续等待...")
else:
# 其他时候记录DEBUG级别日志减少日志数量
logger.debug(f"任务块 {task_block_id} AMR选择未完成已尝试 {retry_count}")
# 等待0.5秒后继续尝试
await asyncio.sleep(actual_retry_interval)
async def wait_for_task_block_action_completion(task_block_id: str, token: str = None) -> Optional[Dict[str, Any]]:
"""
等待任务块动作完成,轮询任务块动作详情直到获取到动作完成
Args:
task_block_id: 任务块ID
token: 认证令牌
Returns:
Optional[Dict[str, Any]]: 包含AMR ID的响应如果请求失败则返回None
"""
logger.info(f"开始等待任务块 {task_block_id} 的动作完成")
retry_count = 0
# 使用固定的重试间隔0.5秒
actual_retry_interval = 0.5
# 无限循环,直到获取到结果
while True:
retry_count += 1
response = await get_task_block_action_detail(task_block_id, token)
if not response or not response.get("success", False):
logger.warning(f"获取任务块动作 详情失败,继续重试,当前尝试次数: {retry_count}")
await asyncio.sleep(actual_retry_interval)
continue
# 从响应中获取关键字段
result = response.get("result", {})
action_status = result.get("status", "")
if action_status in [3, 4, 5]:
logger.info(f"任务块 {task_block_id} 动作已完成,共尝试 {retry_count}")
return response
# 否则继续等待
if retry_count % 10 == 0:
# 每10次请求记录一次INFO级别日志
logger.info(f"任务块 {task_block_id} 动作未完成(actionStatus为空),已尝试 {retry_count} 次,继续等待...")
else:
# 其他时候记录DEBUG级别日志减少日志数量
logger.debug(f"任务块 {task_block_id} 动作未完成,已尝试 {retry_count}")
# 等待0.5秒后继续尝试
await asyncio.sleep(actual_retry_interval)
async def set_task_in_progress(task_id: str, token: str = None) -> Optional[ApiResponse]:
"""
设置系统任务状态为执行中
Args:
task_id: 系统任务ID
token: 认证令牌
Returns:
Optional[ApiResponse]: 接口响应如果请求失败则返回None
"""
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.SET_TASK_IN_PROGRESS_PATH.format(id=task_id)}"
# 构建请求头
headers = {}
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在设置系统任务状态为执行中: {task_id}")
async with aiohttp.ClientSession() as session:
async with session.put(
url,
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.json()
# 尝试解析JSON
try:
if response_text.get("success", False):
logger.info(f"成功设置系统任务状态为执行中: {task_id}")
else:
logger.warning(f"设置系统任务状态为执行中失败: {response_text.get('message', '未知错误')}")
return response_text
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用设置任务状态接口失败: {str(e)}")
return None
async def set_task_completed(task_id: str, token: str = None) -> Optional[ApiResponse]:
"""
设置系统任务状态为已完成
Args:
task_id: 系统任务ID
token: 认证令牌
Returns:
Optional[ApiResponse]: 接口响应如果请求失败则返回None
"""
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.SET_TASK_COMPLETED_PATH.format(id=task_id)}"
# 构建请求头
headers = {}
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在设置系统任务状态为已完成: {task_id}")
async with aiohttp.ClientSession() as session:
async with session.put(
url,
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.json()
# 尝试解析JSON
try:
if response_text.get("success", False):
logger.info(f"成功设置系统任务状态为已完成: {task_id}")
else:
logger.warning(f"设置系统任务状态为已完成失败: {response_text.get('message', '未知错误')}")
return response_text
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用设置系统任务状态为已完成接口失败: {str(e)}")
return None
async def set_task_terminated(task_id: str, token: str = None) -> Optional[ApiResponse]:
"""
设置系统任务状态为已终止
Args:
task_id: 系统任务ID
token: 认证令牌
Returns:
Optional[ApiResponse]: 接口响应如果请求失败则返回None
"""
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.SET_TASK_TERMINATED_PATH.format(id=task_id)}"
# 构建请求头
headers = {}
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在设置系统任务状态为已终止: {task_id}")
async with aiohttp.ClientSession() as session:
async with session.put(
url,
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.json()
# 尝试解析JSON
try:
if response_text.get("success", False):
logger.info(f"成功设置系统任务状态为已终止: {task_id}")
else:
logger.warning(f"设置系统任务状态为已终止失败: {response_text.get('message', '未知错误')}")
return response_text
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用设置任务状态接口失败: {str(e)}")
return None
async def set_task_failed(task_id: str, token: str = None) -> Optional[ApiResponse]:
"""
设置系统任务状态为已失败
Args:
task_id: 系统任务ID
token: 认证令牌
Returns:
Optional[ApiResponse]: 接口响应如果请求失败则返回None
"""
# 调用接口
url = f"{TFApiConfig.BASE_URL}{TFApiConfig.SET_TASK_FAILED_PATH.format(id=task_id)}"
# 构建请求头
headers = {}
headers[TFApiConfig.TOKEN_HEADER] = token
headers["x-tenant-id"] = "1000"
try:
logger.info(f"正在设置任务状态为已失败: {task_id}")
async with aiohttp.ClientSession() as session:
async with session.put(
url,
headers=headers,
timeout=TFApiConfig.TIMEOUT
) as response:
# 读取响应内容
response_text = await response.json()
# 尝试解析JSON
try:
if response_text.get("success", False):
logger.info(f"成功设置系统任务状态为已失败: {task_id}")
else:
logger.warning(f"设置系统任务状态为已失败失败: {response_text.get('message', '未知错误')}")
return response_text
except json.JSONDecodeError:
logger.error(f"解析响应JSON失败: {response_text}")
return None
except Exception as e:
logger.error(f"调用设置任务状态接口失败: {str(e)}")
return None