VWED_server/services/map_data_service.py
2025-07-17 15:47:56 +08:00

575 lines
23 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 uuid
import datetime
from typing import List, Dict, Any, Optional
from sqlalchemy.orm import Session
from sqlalchemy import and_
from data.models import StorageArea, OperatePoint, OperatePointLayer, StorageAreaType
from routes.model.map_model import (
MapDataPushRequest, MapDataPushResponse, MapDataQueryRequest,
MapDataQueryResponse, StorageAreaData, OperatePointData,
OperatePointLayerData, StorageAreaTypeEnum
)
from utils.logger import get_logger
from config.settings import settings
logger = get_logger("services.map_data_service")
class MapDataService:
"""地图数据推送服务"""
@staticmethod
def push_map_data(db: Session, request: MapDataPushRequest) -> MapDataPushResponse:
"""
推送地图数据 - 增量更新模式
该方法采用增量更新的方式,不会删除现有数据,而是在原有基础上增加或更新数据:
- 库区基于area_name判断存在则更新不存在则新增
- 动作点基于station_name判断存在则更新不存在则新增
- 分层基于layer_name判断存在则更新不存在则新增
Args:
db: 数据库会话
request: 地图数据推送请求
Returns:
MapDataPushResponse: 推送结果
"""
try:
# 初始化计数器
storage_areas_created = 0
storage_areas_updated = 0
operate_points_created = 0
operate_points_updated = 0
layers_created = 0
layers_updated = 0
logger.info(f"开始增量推送地图数据: 场景ID={request.scene_id}")
# 处理库区数据 - 增量更新
logger.info(f"开始处理库区数据: 数量={len(request.storage_areas)}")
for area_data in request.storage_areas:
is_new = MapDataService._upsert_storage_area(db, area_data, request.scene_id, request.operate_points)
if is_new:
storage_areas_created += 1
else:
storage_areas_updated += 1
# 检查动作点站点名称重复
duplicate_stations = []
valid_operate_points = []
# 检查请求中的动作点是否有重复的站点名称和库位名称
seen_station_names = set()
seen_location_names = set()
for point_data in request.operate_points:
# 检查是否在请求中重复
if point_data.station_name in seen_station_names:
duplicate_stations.append(point_data.station_name)
logger.warning(f"发现在请求中重复的站点名称: {point_data.station_name}")
continue
seen_station_names.add(point_data.station_name)
valid_operate_points.append(point_data)
# 处理动作点数据 - 增量更新
logger.info(f"开始处理动作点数据: 数量={len(valid_operate_points)}")
for point_data in valid_operate_points:
is_new, operate_point = MapDataService._upsert_operate_point(
db, point_data, request.scene_id
)
if is_new:
operate_points_created += 1
else:
operate_points_updated += 1
# 处理分层数据 - 增量更新
if point_data.layers:
layer_counts = MapDataService._upsert_layers(
db, operate_point, point_data.layers
)
layers_created += layer_counts['created']
layers_updated += layer_counts['updated']
# 提交事务
db.commit()
logger.info(f"地图数据推送成功: 场景ID={request.scene_id}, "
f"库区(新增={storage_areas_created},更新={storage_areas_updated}), "
f"动作点(新增={operate_points_created},更新={operate_points_updated}), "
f"分层(新增={layers_created},更新={layers_updated})")
# 生成响应消息
result_message = f"增量推送成功。库区:新增{storage_areas_created}个,更新{storage_areas_updated}个;"
result_message += f"动作点:新增{operate_points_created}个,更新{operate_points_updated}个;"
result_message += f"分层:新增{layers_created}个,更新{layers_updated}"
# 如果有重复的站点,添加提示信息
if duplicate_stations:
duplicate_count = len(duplicate_stations)
duplicate_names = ", ".join(set(duplicate_stations)) # 去重后的站点名称
result_message += f"。检测到{duplicate_count}个重复的站点名称已被过滤:{duplicate_names}"
return MapDataPushResponse(
scene_id=request.scene_id,
storage_areas_count=storage_areas_created + storage_areas_updated,
operate_points_count=operate_points_created + operate_points_updated,
layers_count=layers_created + layers_updated,
message=result_message
)
except Exception as e:
db.rollback()
logger.error(f"地图数据推送失败: {str(e)}")
raise
@staticmethod
def _upsert_storage_area(db: Session, area_data: StorageAreaData, scene_id: str,
operate_points_data: List[OperatePointData]) -> bool:
"""
增量更新库区数据
Args:
db: 数据库会话
area_data: 库区数据
scene_id: 场景ID
operate_points_data: 动作点数据列表
Returns:
bool: True表示新增False表示更新
"""
# 查找现有库区基于area_name和scene_id
existing_area = db.query(StorageArea).filter(
and_(
StorageArea.area_name == area_data.area_name,
StorageArea.scene_id == scene_id,
StorageArea.is_deleted == False
)
).first()
# 筛选属于该库区的动作点
area_points = [point for point in operate_points_data if point.area_name == area_data.area_name]
# 系统自动计算容量
max_capacity = MapDataService._calculate_storage_area_capacity(
area_data.area_type.value, area_points
)
if existing_area:
# 更新现有库区
existing_area.area_type = StorageAreaType(area_data.area_type)
existing_area.max_capacity = max_capacity
existing_area.description = area_data.description
existing_area.tags = area_data.tags
existing_area.select_logic = area_data.select_logic
existing_area.updated_at = datetime.datetime.now()
logger.info(f"更新库区: {area_data.area_name}")
return False
else:
# 创建新库区
new_area = StorageArea(
id=str(uuid.uuid4()),
area_name=area_data.area_name,
area_type=StorageAreaType(area_data.area_type),
scene_id=scene_id,
max_capacity=max_capacity,
description=area_data.description,
tags=area_data.tags,
select_logic=area_data.select_logic
)
db.add(new_area)
logger.info(f"创建新库区: {area_data.area_name}")
return True
@staticmethod
def _upsert_operate_point(db: Session, point_data: OperatePointData, scene_id: str) -> tuple[bool, OperatePoint]:
"""
增量更新动作点数据
Args:
db: 数据库会话
point_data: 动作点数据
scene_id: 场景ID
Returns:
tuple[bool, OperatePoint]: (是否新增, 动作点对象)
"""
# 查找现有动作点基于station_name和scene_id
existing_point = db.query(OperatePoint).filter(
and_(
OperatePoint.station_name == point_data.station_name,
OperatePoint.scene_id == scene_id,
OperatePoint.is_deleted == False
)
).first()
# 根据库区名称获取库区信息
storage_area = None
if point_data.area_name:
storage_area = db.query(StorageArea).filter(
and_(
StorageArea.area_name == point_data.area_name,
StorageArea.scene_id == scene_id,
StorageArea.is_deleted == False
)
).first()
if existing_point:
# 更新现有动作点
existing_point.storage_area_id = storage_area.id if storage_area else None
existing_point.storage_area_type = storage_area.area_type if storage_area else None
existing_point.area_name = point_data.area_name
existing_point.max_layers = point_data.max_layers
existing_point.position_x = point_data.position_x
existing_point.position_y = point_data.position_y
existing_point.position_z = point_data.position_z
existing_point.content = point_data.content or ""
existing_point.tags = point_data.tags or ""
existing_point.description = point_data.description
existing_point.updated_at = datetime.datetime.now()
logger.info(f"更新动作点: {point_data.station_name}")
return False, existing_point
else:
# 创建新动作点
new_point = OperatePoint(
id=str(uuid.uuid4()),
station_name=point_data.station_name,
scene_id=scene_id,
# storage_area_id=storage_area.id if storage_area else None,
storage_area_type=storage_area.area_type if storage_area else None,
area_name=point_data.area_name,
max_layers=point_data.max_layers,
position_x=point_data.position_x,
position_y=point_data.position_y,
position_z=point_data.position_z,
content=point_data.content or "",
tags=point_data.tags or "",
description=point_data.description
)
db.add(new_point)
logger.info(f"创建新动作点: {point_data.station_name}")
return True, new_point
@staticmethod
def _upsert_layers(db: Session, operate_point: OperatePoint,
layers_data: List[OperatePointLayerData]) -> Dict[str, int]:
"""
增量更新分层数据
Args:
db: 数据库会话
operate_point: 动作点对象
layers_data: 分层数据列表
Returns:
Dict[str, int]: 创建和更新的分层数量统计
"""
created_count = 0
updated_count = 0
if not layers_data:
return {'created': created_count, 'updated': updated_count}
for index, layer_data in enumerate(layers_data, 1):
# 自动生成层索引从1开始
layer_index = index
# 查找现有分层基于layer_name和operate_point_id
existing_layer = db.query(OperatePointLayer).filter(
and_(
OperatePointLayer.layer_name == layer_data.layer_name,
OperatePointLayer.operate_point_id == operate_point.id,
OperatePointLayer.is_deleted == False
)
).first()
if existing_layer:
# 更新现有分层
existing_layer.layer_index = layer_index
existing_layer.max_weight = layer_data.max_weight
existing_layer.max_volume = layer_data.max_volume
existing_layer.layer_height = layer_data.layer_height
existing_layer.description = layer_data.description
existing_layer.tags = layer_data.tags
existing_layer.updated_at = datetime.datetime.now()
logger.debug(f"更新分层: {layer_data.layer_name} (layer_index={layer_index})")
updated_count += 1
else:
# 创建新分层
new_layer = OperatePointLayer(
id=str(uuid.uuid4()),
operate_point_id=operate_point.id,
station_name=operate_point.station_name,
area_name=operate_point.area_name,
scene_id=operate_point.scene_id,
layer_index=layer_index,
layer_name=layer_data.layer_name,
max_weight=layer_data.max_weight,
max_volume=layer_data.max_volume,
layer_height=layer_data.layer_height,
description=layer_data.description,
tags=layer_data.tags
)
db.add(new_layer)
# 为新创建的库位层同步扩展属性
try:
MapDataService._sync_extended_properties_to_new_layer(db, new_layer)
logger.debug(f"为新库位层 {new_layer.id} 同步扩展属性成功")
except Exception as e:
logger.error(f"为新库位层 {new_layer.id} 同步扩展属性失败: {str(e)}")
logger.debug(f"创建新分层: {layer_data.layer_name} (layer_index={layer_index})")
created_count += 1
return {'created': created_count, 'updated': updated_count}
@staticmethod
def query_map_data(db: Session, request: MapDataQueryRequest) -> MapDataQueryResponse:
"""
查询地图数据
Args:
db: 数据库会话
request: 地图数据查询请求
Returns:
MapDataQueryResponse: 查询结果
"""
try:
# 查询库区数据
storage_areas_query = db.query(StorageArea).filter(
and_(
StorageArea.scene_id == request.scene_id,
StorageArea.is_deleted == False
)
)
if request.area_type:
storage_areas_query = storage_areas_query.filter(
StorageArea.area_type == request.area_type
)
storage_areas = storage_areas_query.all()
# 查询动作点数据
operate_points_query = db.query(OperatePoint).filter(
and_(
OperatePoint.scene_id == request.scene_id,
OperatePoint.is_deleted == False
)
)
operate_points = operate_points_query.all()
# 统计数据
total_capacity = sum(area.max_capacity for area in storage_areas)
used_capacity = sum(area.current_usage for area in storage_areas)
dense_areas_count = sum(1 for area in storage_areas if area.area_type == StorageAreaType.DENSE)
general_areas_count = len(storage_areas) - dense_areas_count
# 查询分层数据
total_layers = 0
occupied_layers = 0
if request.include_layers:
for point in operate_points:
layers = db.query(OperatePointLayer).filter(
and_(
OperatePointLayer.operate_point_id == point.id,
OperatePointLayer.is_deleted == False
)
).all()
total_layers += len(layers)
occupied_layers += sum(1 for layer in layers if layer.is_occupied)
# 转换为响应格式
storage_areas_data = []
for area in storage_areas:
area_dict = area.to_dict()
area_dict['area_type'] = area.area_type.value
storage_areas_data.append(area_dict)
operate_points_data = []
for point in operate_points:
point_dict = point.to_dict()
# 添加库区类型信息
if point.storage_area_type:
point_dict['storage_area_type'] = point.storage_area_type.value
# 添加库区名称信息
if point.area_name:
point_dict['area_name'] = point.area_name
if request.include_layers:
# 包含分层数据
layers = db.query(OperatePointLayer).filter(
and_(
OperatePointLayer.operate_point_id == point.id,
OperatePointLayer.is_deleted == False
)
).order_by(OperatePointLayer.layer_index).all()
point_dict['layers'] = [layer.to_dict() for layer in layers]
operate_points_data.append(point_dict)
return MapDataQueryResponse(
scene_id=request.scene_id,
storage_areas=storage_areas_data,
operate_points=operate_points_data,
total_capacity=total_capacity,
used_capacity=used_capacity,
dense_areas_count=dense_areas_count,
general_areas_count=general_areas_count,
total_layers=total_layers,
occupied_layers=occupied_layers
)
except Exception as e:
logger.error(f"查询地图数据失败: {str(e)}")
raise
@staticmethod
def _delete_existing_data(db: Session, scene_id: str):
"""删除现有数据"""
# 先获取需要删除的动作点ID列表
operate_point_ids = db.query(OperatePoint.id).filter(
and_(
OperatePoint.scene_id == scene_id,
OperatePoint.is_deleted == False
)
).all()
operate_point_ids = [point_id[0] for point_id in operate_point_ids]
# 物理删除动作点分层(为了避免主键冲突)
if operate_point_ids:
db.query(OperatePointLayer).filter(
OperatePointLayer.operate_point_id.in_(operate_point_ids)
).delete(synchronize_session=False)
# 物理删除动作点(为了避免主键冲突)
db.query(OperatePoint).filter(
and_(
OperatePoint.scene_id == scene_id,
OperatePoint.is_deleted == False
)
).delete(synchronize_session=False)
# 物理删除库区(为了避免主键冲突)
db.query(StorageArea).filter(
and_(
StorageArea.scene_id == scene_id,
StorageArea.is_deleted == False
)
).delete(synchronize_session=False)
@staticmethod
def _calculate_storage_area_capacity(area_type: str, operate_points_data: List) -> int:
"""
计算库区容量
Args:
area_type: 库区类型
operate_points_data: 属于该库区的动作点数据列表
Returns:
int: 计算出的容量
"""
# 根据库区类型从配置中获取参数
if area_type == "dense":
base_capacity = settings.MAP_DENSE_STORAGE_BASE_CAPACITY
capacity_per_point = settings.MAP_DENSE_STORAGE_CAPACITY_PER_POINT
layer_multiplier = settings.MAP_DENSE_STORAGE_LAYER_MULTIPLIER
else: # general
base_capacity = settings.MAP_GENERAL_STORAGE_BASE_CAPACITY
capacity_per_point = settings.MAP_GENERAL_STORAGE_CAPACITY_PER_POINT
layer_multiplier = settings.MAP_GENERAL_STORAGE_LAYER_MULTIPLIER
# 基础容量
total_capacity = base_capacity
# 根据动作点数量和层数计算额外容量
for point_data in operate_points_data:
point_capacity = capacity_per_point
# 如果有多层,应用层数倍数
if point_data.max_layers > 1:
point_capacity = int(point_capacity * layer_multiplier * point_data.max_layers)
total_capacity += point_capacity
return total_capacity
@staticmethod
def _sync_extended_properties_to_new_layer(db: Session, layer: OperatePointLayer):
"""
将所有已启用的扩展属性同步到新创建的库位层
Args:
db: 数据库会话
layer: 新创建的库位层对象
"""
try:
# 导入扩展属性模型(在方法内导入避免循环导入)
from data.models import ExtendedProperty
import json
import datetime
# 获取所有已启用的扩展属性
extended_properties = db.query(ExtendedProperty).filter(
ExtendedProperty.is_deleted == False,
ExtendedProperty.is_enabled == True
).all()
if not extended_properties:
# 如果没有扩展属性,则不需要处理
return
# 解析现有的config_json
config = {}
if layer.config_json:
try:
config = json.loads(layer.config_json)
except Exception as e:
logger.error(f"解析库位层 {layer.id} 的config_json失败: {str(e)}")
config = {}
# 确保extended_fields字段存在
if 'extended_fields' not in config:
config['extended_fields'] = {}
# 同步所有扩展属性
for prop in extended_properties:
config['extended_fields'][prop.property_name] = {
'value': prop.default_value,
'type': prop.property_type.value,
'is_required': prop.is_required,
'updated_at': datetime.datetime.now().isoformat()
}
# 更新config_json
layer.config_json = json.dumps(config, ensure_ascii=False, indent=2)
logger.debug(f"为库位层 {layer.id} 同步了 {len(extended_properties)} 个扩展属性")
except Exception as e:
logger.error(f"同步扩展属性到库位层失败: {str(e)}")
raise