#!/usr/bin/env python # -*- coding: utf-8 -*- """ 动作点管理服务 处理动作点和库位的管理操作 """ import math from typing import List, Dict, Any, Optional, Tuple from sqlalchemy.orm import Session from sqlalchemy import and_, or_, func from data.models import OperatePoint, OperatePointLayer, StorageArea, StorageAreaType, ExtendedProperty, ExtendedPropertyTypeEnum, StorageLocationLog from routes.model.operate_point_model import ( OperatePointLayerInfo, StorageLocationListRequest, StorageLocationListResponse, StorageLocationInfo, StorageLocationStatistics, StorageLocationStatusUpdateRequest, StorageLocationStatusUpdateResponse, BatchStorageLocationStatusUpdateRequest, BatchStorageLocationStatusUpdateResponse, StorageLocationActionEnum, ExtendedPropertyCreateRequest, ExtendedPropertyCreateResponse, ExtendedPropertyListRequest, ExtendedPropertyListResponse, ExtendedPropertyInfo, ExtendedPropertyDeleteResponse, StorageLocationDetailResponse, StorageLocationEditRequest, StorageLocationEditResponse, StorageLocationLogInfo, StorageLocationLogListRequest, StorageLocationLogListResponse, ) from utils.logger import get_logger import datetime logger = get_logger("services.operate_point_service") class OperatePointService: """库位管理服务""" @staticmethod def get_storage_location_list(db: Session, request: StorageLocationListRequest) -> StorageLocationListResponse: """ 获取库位列表 Args: db: 数据库会话 request: 库位列表查询请求 Returns: StorageLocationListResponse: 库位列表响应 """ try: # 构建查询条件 - 主要查询 OperatePointLayer 表 query = db.query(OperatePointLayer).filter(OperatePointLayer.is_deleted == False) # 通过关联的 OperatePoint 进行筛选 if request.scene_id: query = query.join(OperatePoint).filter(OperatePoint.scene_id == request.scene_id) if request.storage_area_id: query = query.join(OperatePoint).filter(OperatePoint.storage_area_id == request.storage_area_id) # 站点名称模糊搜索 if request.station_name: query = query.filter(OperatePointLayer.station_name.like(f"%{request.station_name}%")) # 库位名称模糊搜索 if request.storage_location_name: query = query.filter(OperatePointLayer.storage_location_name.like(f"%{request.storage_location_name}%")) # 层名称模糊搜索 if request.layer_name: query = query.filter(OperatePointLayer.layer_name.like(f"%{request.layer_name}%")) # 是否禁用过滤 if request.is_disabled is not None: query = query.filter(OperatePointLayer.is_disabled == request.is_disabled) # 是否占用过滤 if request.is_occupied is not None: query = query.filter(OperatePointLayer.is_occupied == request.is_occupied) # 是否锁定过滤 if request.is_locked is not None: query = query.filter(OperatePointLayer.is_locked == request.is_locked) # 是否空托盘过滤 if request.is_empty_tray is not None: query = query.filter(OperatePointLayer.is_empty_tray == request.is_empty_tray) # 获取总数 total = query.count() # 分页 offset = (request.page - 1) * request.page_size storage_locations = query.offset(offset).limit(request.page_size).all() # 转换为响应对象 storage_location_infos = [] for sl in storage_locations: storage_location_info = OperatePointService._convert_to_storage_location_info( sl, request.include_operate_point_info, request.include_extended_fields ) storage_location_infos.append(storage_location_info) # 计算总页数 total_pages = math.ceil(total / request.page_size) if total > 0 else 0 # 获取统计信息 # statistics = OperatePointService._get_storage_location_statistics(db, request) return StorageLocationListResponse( total=total, page=request.page, page_size=request.page_size, total_pages=total_pages, storage_locations=storage_location_infos, # statistics=statistics ) except Exception as e: logger.error(f"获取库位列表失败: {str(e)}") raise @staticmethod def _convert_to_storage_location_info(layer: OperatePointLayer, include_operate_point_info: bool = True, include_extended_fields: bool = False) -> StorageLocationInfo: """ 将数据库模型转换为库位信息响应对象 Args: layer: 动作点分层数据库模型 include_operate_point_info: 是否包含动作点信息 Returns: StorageLocationInfo: 库位信息 """ # 构建库位信息 storage_location_info = StorageLocationInfo( id=layer.id, layer_index=layer.layer_index, layer_name=layer.layer_name, is_occupied=layer.is_occupied, is_locked=layer.is_locked, is_disabled=layer.is_disabled, is_empty_tray=layer.is_empty_tray, locked_by=layer.locked_by, goods_content=layer.goods_content, goods_weight=layer.goods_weight, goods_volume=layer.goods_volume, goods_stored_at=layer.goods_stored_at, goods_retrieved_at=layer.goods_retrieved_at, last_access_at=layer.last_access_at, max_weight=layer.max_weight, max_volume=layer.max_volume, layer_height=layer.layer_height, tags=layer.tags, description=layer.description, created_at=layer.created_at, updated_at=layer.updated_at ) # 如果需要包含动作点信息,则直接平铺到库位信息中 if include_operate_point_info and layer.operate_point: storage_location_info.operate_point_id = layer.operate_point.id storage_location_info.station_name = layer.operate_point.station_name storage_location_info.storage_location_name = layer.operate_point.storage_location_name storage_location_info.scene_id = layer.operate_point.scene_id storage_location_info.storage_area_id = layer.operate_point.storage_area_id storage_location_info.storage_area_type = layer.operate_point.storage_area_type.value if layer.operate_point.storage_area_type else None storage_location_info.area_name = layer.operate_point.area_name storage_location_info.max_layers = layer.operate_point.max_layers storage_location_info.current_layers = layer.operate_point.current_layers storage_location_info.position_x = layer.operate_point.position_x storage_location_info.position_y = layer.operate_point.position_y storage_location_info.position_z = layer.operate_point.position_z storage_location_info.operate_point_description = layer.operate_point.description # 如果需要包含扩展字段,则从 config_json 中解析 if include_extended_fields and layer.config_json: extended_fields = OperatePointService._parse_extended_fields(layer.config_json) # 将扩展字段转换为简单的键值对格式,便于前端处理 storage_location_info.extended_fields = { field_name: field_data["value"] for field_name, field_data in extended_fields.items() } elif include_extended_fields: storage_location_info.extended_fields = {} return storage_location_info @staticmethod def update_storage_location_status(db: Session, request: StorageLocationStatusUpdateRequest) -> StorageLocationStatusUpdateResponse: """ 更新库位状态 Args: db: 数据库会话 request: 库位状态更新请求 Returns: StorageLocationStatusUpdateResponse: 更新响应 """ try: # 查询库位(使用layer_name进行查询) storage_location = db.query(OperatePointLayer).filter( OperatePointLayer.layer_name == request.layer_name, OperatePointLayer.is_deleted == False ).first() if not storage_location: return StorageLocationStatusUpdateResponse( layer_name=request.layer_name, action=request.action, success=False, message="库位不存在", new_status={} ) # 执行状态更新 success, message = OperatePointService._execute_status_update(storage_location, request.action, request.locked_by) if success: db.commit() # 记录操作日志 try: operator = OperatePointService._get_operator(request.locked_by) OperatePointService.create_storage_location_log( db=db, operator=operator, operation_type=request.action.value, affected_storage_locations=[request.layer_name], description=request.reason ) except Exception as e: logger.error(f"记录操作日志失败: {str(e)}") # 获取更新后的状态 new_status = OperatePointService._get_storage_location_status(storage_location) return StorageLocationStatusUpdateResponse( layer_name=request.layer_name, action=request.action, success=True, message=message, new_status=new_status ) else: db.rollback() return StorageLocationStatusUpdateResponse( layer_name=request.layer_name, action=request.action, success=False, message=message, new_status={} ) except Exception as e: db.rollback() logger.error(f"更新库位状态失败: {str(e)}") return StorageLocationStatusUpdateResponse( layer_name=request.layer_name, action=request.action, success=False, message=f"更新库位状态失败: {str(e)}", new_status={} ) @staticmethod def batch_update_storage_location_status(db: Session, request: BatchStorageLocationStatusUpdateRequest) -> BatchStorageLocationStatusUpdateResponse: """ 批量更新库位状态 Args: db: 数据库会话 request: 批量库位状态更新请求 Returns: BatchStorageLocationStatusUpdateResponse: 批量更新响应 """ try: results = [] success_count = 0 failed_count = 0 for layer_name in request.layer_names: # 创建单个更新请求 single_request = StorageLocationStatusUpdateRequest( layer_name=layer_name, action=request.action, locked_by=request.locked_by, reason=request.reason ) # 执行单个更新 result = OperatePointService.update_storage_location_status(db, single_request) results.append(result) if result.success: success_count += 1 else: failed_count += 1 return BatchStorageLocationStatusUpdateResponse( total_count=len(request.layer_names), success_count=success_count, failed_count=failed_count, results=results ) except Exception as e: logger.error(f"批量更新库位状态失败: {str(e)}") raise @staticmethod def _execute_status_update(storage_location: OperatePointLayer, action: StorageLocationActionEnum, locked_by: Optional[str] = None) -> Tuple[bool, str]: """ 执行状态更新操作 Args: storage_location: 库位对象 action: 操作类型(枚举) locked_by: 锁定者 Returns: Tuple[bool, str]: (是否成功, 消息) """ try: current_time = datetime.datetime.now() if action == StorageLocationActionEnum.OCCUPY: # 占用操作 if storage_location.is_occupied: return True, "库位已被占用,无需重复操作" if storage_location.is_disabled: return False, "库位已禁用,无法占用" if storage_location.is_locked: return False, "库位已锁定,无法占用" storage_location.is_occupied = True storage_location.last_access_at = current_time storage_location.goods_stored_at = current_time storage_location.goods_retrieved_at = None return True, "库位占用成功" elif action == StorageLocationActionEnum.RELEASE: # 释放操作 if not storage_location.is_occupied: return True, "库位未被占用,无需重复操作" storage_location.is_occupied = False storage_location.goods_content = '' storage_location.goods_weight = None storage_location.goods_volume = None storage_location.goods_retrieved_at = current_time storage_location.goods_stored_at = None storage_location.last_access_at = current_time return True, "库位释放成功" elif action == StorageLocationActionEnum.LOCK: # 锁定操作 if not locked_by: return False, "锁定操作必须提供锁定者" if storage_location.is_locked: if storage_location.locked_by == locked_by: return True, "库位已被相同锁定者锁定,无需重复操作" else: return False, f"库位已被其他锁定者锁定,当前锁定者: {storage_location.locked_by}" storage_location.is_locked = True storage_location.locked_by = locked_by storage_location.last_access_at = current_time return True, "库位锁定成功" elif action == StorageLocationActionEnum.UNLOCK: # 解锁操作 if not storage_location.is_locked: return True, "库位未被锁定,无需重复操作" storage_location.is_locked = False storage_location.locked_by = None storage_location.last_access_at = current_time return True, "库位解锁成功" elif action == StorageLocationActionEnum.ENABLE: # 启用操作 if not storage_location.is_disabled: return True, "库位已启用,无需重复操作" storage_location.is_disabled = False storage_location.last_access_at = current_time return True, "库位启用成功" elif action == StorageLocationActionEnum.DISABLE: # 禁用操作 if storage_location.is_disabled: return True, "库位已禁用,无需重复操作" storage_location.is_disabled = True storage_location.last_access_at = current_time return True, "库位禁用成功" elif action == StorageLocationActionEnum.SET_EMPTY_TRAY: # 设置为空托盘 if storage_location.is_empty_tray: return True, "库位已设置为空托盘,无需重复操作" storage_location.is_empty_tray = True storage_location.is_occupied = True storage_location.last_access_at = current_time storage_location.goods_stored_at = current_time storage_location.goods_retrieved_at = None return True, "库位设置为空托盘成功" elif action == StorageLocationActionEnum.CLEAR_EMPTY_TRAY: # 清除空托盘状态 if not storage_location.is_empty_tray: return True, "库位已非空托盘状态,无需重复操作" storage_location.is_empty_tray = False storage_location.last_access_at = current_time storage_location.is_occupied = False storage_location.goods_content = '' storage_location.goods_weight = None storage_location.goods_volume = None storage_location.goods_retrieved_at = current_time storage_location.goods_stored_at = None return True, "库位清除空托盘状态成功" else: return False, f"不支持的操作类型: {action}" except Exception as e: logger.error(f"执行状态更新操作失败: {str(e)}") return False, f"执行状态更新操作失败: {str(e)}" @staticmethod def _get_storage_location_status(storage_location: OperatePointLayer) -> Dict[str, Any]: """ 获取库位状态信息 Args: storage_location: 库位对象 Returns: Dict[str, Any]: 状态信息 """ return { "id": storage_location.id, "is_occupied": storage_location.is_occupied, "is_locked": storage_location.is_locked, "is_disabled": storage_location.is_disabled, "is_empty_tray": storage_location.is_empty_tray, "locked_by": storage_location.locked_by, "goods_content": storage_location.goods_content, "last_access_at": storage_location.last_access_at, "updated_at": storage_location.updated_at } @staticmethod def _get_operator(provided_operator: Optional[str] = None, fallback_operator: str = "系统") -> str: """ 获取操作人信息 Args: provided_operator: 提供的操作人(如锁定者) fallback_operator: 默认操作人 Returns: str: 操作人名称 """ # 如果提供了操作人且不为空,使用提供的操作人 if provided_operator and provided_operator.strip(): return provided_operator.strip() # TODO: 未来可以从当前用户会话中获取操作人信息 # 例如:从 JWT token 或 session 中获取当前登录用户 # current_user = get_current_user() # if current_user: # return current_user.username # 使用默认操作人 return fallback_operator @staticmethod def get_operation_result_summary(results: List[StorageLocationStatusUpdateResponse]) -> Dict[str, Any]: """ 获取操作结果的汇总统计信息 Args: results: 操作结果列表 Returns: Dict[str, Any]: 统计信息 """ if not results: return { "total": 0, "success": 0, "failed": 0, "no_change_needed": 0, "actual_updates": 0 } total = len(results) success = sum(1 for r in results if r.success) failed = total - success # 统计无需更改的操作(幂等性操作) no_change_messages = [ "无需重复操作", "已被占用,无需重复操作", "未被占用,无需重复操作", "已被相同锁定者锁定,无需重复操作", "未被锁定,无需重复操作", "已启用,无需重复操作", "已禁用,无需重复操作", "已设置为空托盘,无需重复操作", "已非空托盘状态,无需重复操作" ] no_change_needed = sum(1 for r in results if r.success and any(msg in r.message for msg in no_change_messages)) actual_updates = success - no_change_needed return { "total": total, "success": success, "failed": failed, "no_change_needed": no_change_needed, "actual_updates": actual_updates } @staticmethod def _parse_extended_fields(config_json: str) -> Dict[str, Any]: """ 解析扩展字段JSON Args: config_json: JSON字符串 Returns: Dict[str, Any]: 扩展字段字典 """ try: if not config_json: return {} import json config = json.loads(config_json) return config.get("extended_fields", {}) except Exception as e: logger.error(f"解析扩展字段JSON失败: {str(e)}") return {} @staticmethod def _serialize_extended_fields(extended_fields: Dict[str, Any]) -> str: """ 序列化扩展字段为JSON Args: extended_fields: 扩展字段字典 Returns: str: JSON字符串 """ try: import json config = {"extended_fields": extended_fields} return json.dumps(config, ensure_ascii=False, indent=2) except Exception as e: logger.error(f"序列化扩展字段JSON失败: {str(e)}") return "{}" @staticmethod def _infer_field_type(value: Any) -> str: """ 推断字段类型 Args: value: 字段值 Returns: str: 字段类型 """ if isinstance(value, bool): return "boolean" elif isinstance(value, int): return "integer" elif isinstance(value, float): return "number" elif isinstance(value, str): # 尝试判断是否为日期 try: datetime.datetime.fromisoformat(value) return "date" except: return "string" elif isinstance(value, list): return "array" elif isinstance(value, dict): return "object" else: return "string" # 扩展属性管理相关方法 @staticmethod def create_extended_property(db: Session, request: ExtendedPropertyCreateRequest) -> ExtendedPropertyCreateResponse: """ 创建扩展属性 Args: db: 数据库会话 request: 扩展属性创建请求 Returns: ExtendedPropertyCreateResponse: 创建响应 """ try: # 检查属性名称是否已存在 existing_property = db.query(ExtendedProperty).filter( ExtendedProperty.property_name == request.property_name, ExtendedProperty.is_deleted == False ).first() if existing_property: raise ValueError(f"属性名称 '{request.property_name}' 已存在") # 创建扩展属性 extended_property = ExtendedProperty( property_key=request.property_name, # 使用 property_name 作为 property_key property_name=request.property_name, property_type=request.property_type, is_required=request.is_required, is_enabled=request.is_enabled, description=request.description, placeholder=request.placeholder, default_value=request.default_value, options=OperatePointService._serialize_options(request.options), validation_rules=OperatePointService._serialize_validation_rules(request.validation_rules), category=request.category, sort_order=request.sort_order, display_width=request.display_width, display_format=request.display_format ) db.add(extended_property) db.commit() db.refresh(extended_property) # 成功创建扩展属性后,更新所有 OperatePointLayer 的 config_json try: OperatePointService._add_extended_property_to_layers( db=db, property_name=extended_property.property_name, property_type=extended_property.property_type, default_value=extended_property.default_value, is_required=extended_property.is_required ) db.commit() logger.info(f"成功为所有库位层添加扩展属性: {extended_property.property_name}") except Exception as e: logger.error(f"添加扩展属性到库位层失败: {str(e)}") # 这里不抛出异常,因为扩展属性已经创建成功 # 记录操作日志 try: operator = OperatePointService._get_operator() OperatePointService.create_storage_location_log( db=db, operator=operator, operation_type="创建扩展属性", affected_storage_locations=[], # 扩展属性影响所有库位,这里传空列表 description=f"创建扩展属性 '{extended_property.property_name}',类型: {extended_property.property_type.value},已应用到所有库位层" ) except Exception as e: logger.error(f"记录操作日志失败: {str(e)}") return ExtendedPropertyCreateResponse( id=str(extended_property.id), property_name=extended_property.property_name, message="扩展属性创建成功,已应用到所有库位层" ) except Exception as e: db.rollback() logger.error(f"创建扩展属性失败: {str(e)}") raise @staticmethod def get_extended_property_list(db: Session, request: ExtendedPropertyListRequest) -> ExtendedPropertyListResponse: """ 获取扩展属性列表 Args: db: 数据库会话 request: 扩展属性列表查询请求 Returns: ExtendedPropertyListResponse: 属性列表响应 """ try: # 构建查询条件 query = db.query(ExtendedProperty).filter(ExtendedProperty.is_deleted == False) # 属性名称模糊搜索 if request.property_name: query = query.filter(ExtendedProperty.property_name.like(f"%{request.property_name}%")) # 属性类型过滤 if request.property_type: query = query.filter(ExtendedProperty.property_type == request.property_type) # 分类过滤 if request.category: query = query.filter(ExtendedProperty.category == request.category) # 是否启用过滤 if request.is_enabled is not None: query = query.filter(ExtendedProperty.is_enabled == request.is_enabled) # 排序 query = query.order_by(ExtendedProperty.sort_order.asc(), ExtendedProperty.created_at.desc()) # 获取总数 total = query.count() # 分页 offset = (request.page - 1) * request.page_size properties = query.offset(offset).limit(request.page_size).all() # 转换为响应对象 property_infos = [] for prop in properties: property_info = OperatePointService._convert_to_extended_property_info(prop) property_infos.append(property_info) # 计算总页数 total_pages = math.ceil(total / request.page_size) if total > 0 else 0 return ExtendedPropertyListResponse( total=total, page=request.page, page_size=request.page_size, total_pages=total_pages, properties=property_infos ) except Exception as e: logger.error(f"获取扩展属性列表失败: {str(e)}") raise @staticmethod def delete_extended_property(db: Session, property_id: str) -> ExtendedPropertyDeleteResponse: """ 删除扩展属性 Args: db: 数据库会话 property_id: 属性ID Returns: ExtendedPropertyDeleteResponse: 删除响应 """ try: # 查询扩展属性 extended_property = db.query(ExtendedProperty).filter( ExtendedProperty.id == property_id, ExtendedProperty.is_deleted == False ).first() if not extended_property: raise ValueError("扩展属性不存在") # 软删除 extended_property.is_deleted = True extended_property.updated_at = datetime.datetime.now() # 记录属性名称,用于后续清理 property_name = extended_property.property_name db.commit() # 成功删除扩展属性后,从所有 OperatePointLayer 的 config_json 中移除该属性 try: OperatePointService._remove_extended_property_from_layers( db=db, property_name=property_name ) db.commit() logger.info(f"成功从所有库位层移除扩展属性: {property_name}") except Exception as e: logger.error(f"从库位层移除扩展属性失败: {str(e)}") # 这里不抛出异常,因为扩展属性已经删除成功 # 记录操作日志 try: operator = OperatePointService._get_operator() OperatePointService.create_storage_location_log( db=db, operator=operator, operation_type="删除扩展属性", affected_storage_locations=[], # 扩展属性影响所有库位,这里传空列表 description=f"删除扩展属性 '{property_name}',已从所有库位层移除" ) except Exception as e: logger.error(f"记录操作日志失败: {str(e)}") return ExtendedPropertyDeleteResponse( property_id=str(extended_property.id), property_name=extended_property.property_name, message="扩展属性删除成功,已从所有库位层移除" ) except Exception as e: db.rollback() logger.error(f"删除扩展属性失败: {str(e)}") raise @staticmethod def _convert_to_extended_property_info(prop: ExtendedProperty) -> ExtendedPropertyInfo: """ 将数据库模型转换为扩展属性信息响应对象 Args: prop: 扩展属性数据库模型 Returns: ExtendedPropertyInfo: 扩展属性信息 """ return ExtendedPropertyInfo( id=str(prop.id), property_name=prop.property_name, property_type=prop.property_type, is_required=prop.is_required, is_enabled=prop.is_enabled, description=prop.description, placeholder=prop.placeholder, default_value=prop.default_value, options=OperatePointService._parse_options(prop.options), validation_rules=OperatePointService._parse_validation_rules(prop.validation_rules), category=prop.category, sort_order=prop.sort_order, display_width=prop.display_width, display_format=prop.display_format, created_at=prop.created_at, updated_at=prop.updated_at ) @staticmethod def _serialize_options(options: Optional[List[Dict[str, Any]]]) -> Optional[str]: """ 序列化选项为JSON字符串 Args: options: 选项列表 Returns: str: JSON字符串 """ if not options: return None try: import json return json.dumps(options, ensure_ascii=False) except Exception as e: logger.error(f"序列化选项失败: {str(e)}") return None @staticmethod def _parse_options(options_json: Optional[str]) -> Optional[List[Dict[str, Any]]]: """ 解析选项JSON字符串 Args: options_json: JSON字符串 Returns: List[Dict[str, Any]]: 选项列表 """ if not options_json: return None try: import json return json.loads(options_json) except Exception as e: logger.error(f"解析选项JSON失败: {str(e)}") return None @staticmethod def _serialize_validation_rules(rules: Optional[Dict[str, Any]]) -> Optional[str]: """ 序列化验证规则为JSON字符串 Args: rules: 验证规则字典 Returns: str: JSON字符串 """ if not rules: return None try: import json return json.dumps(rules, ensure_ascii=False) except Exception as e: logger.error(f"序列化验证规则失败: {str(e)}") return None @staticmethod def _parse_validation_rules(rules_json: Optional[str]) -> Optional[Dict[str, Any]]: """ 解析验证规则JSON字符串 Args: rules_json: JSON字符串 Returns: Dict[str, Any]: 验证规则字典 """ if not rules_json: return None try: import json return json.loads(rules_json) except Exception as e: logger.error(f"解析验证规则JSON失败: {str(e)}") return None @staticmethod def _add_extended_property_to_layers( db: Session, property_name: str, property_type: ExtendedPropertyTypeEnum, default_value: Optional[str] = None, is_required: bool = False ): """ 将扩展属性添加到所有库位层的config_json中 Args: db: 数据库会话 property_name: 属性名称 property_type: 属性类型 default_value: 默认值 is_required: 是否必填 """ try: # 获取所有未删除的库位层 layers = db.query(OperatePointLayer).filter( OperatePointLayer.is_deleted == False ).all() for layer in layers: # 解析现有的config_json config = {} if layer.config_json: try: import json 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'] = {} # 添加新的扩展属性 config['extended_fields'][property_name] = { 'value': default_value, 'type': property_type.value, 'is_required': is_required, 'updated_at': datetime.datetime.now().isoformat() } # 更新config_json try: import json layer.config_json = json.dumps(config, ensure_ascii=False, indent=2) logger.debug(f"为库位层 {layer.id} 添加扩展属性: {property_name}") except Exception as e: logger.error(f"序列化库位层 {layer.id} 的config_json失败: {str(e)}") except Exception as e: logger.error(f"添加扩展属性到库位层失败: {str(e)}") raise @staticmethod def _remove_extended_property_from_layers(db: Session, property_name: str): """ 从所有库位层的config_json中移除扩展属性 Args: db: 数据库会话 property_name: 属性名称 """ try: # 获取所有未删除的库位层 layers = db.query(OperatePointLayer).filter( OperatePointLayer.is_deleted == False ).all() for layer in layers: # 解析现有的config_json config = {} if layer.config_json: try: import json config = json.loads(layer.config_json) except Exception as e: logger.error(f"解析库位层 {layer.id} 的config_json失败: {str(e)}") continue # 检查是否存在extended_fields if 'extended_fields' in config and property_name in config['extended_fields']: # 移除指定的扩展属性 del config['extended_fields'][property_name] # 更新config_json try: import json layer.config_json = json.dumps(config, ensure_ascii=False, indent=2) logger.debug(f"从库位层 {layer.id} 移除扩展属性: {property_name}") except Exception as e: logger.error(f"序列化库位层 {layer.id} 的config_json失败: {str(e)}") except Exception as e: logger.error(f"从库位层移除扩展属性失败: {str(e)}") raise @staticmethod def get_storage_location_detail(db: Session, layer_name: str) -> Dict[str, Any]: """ 获取库位详情 Args: db: 数据库会话 layer_name: 库位名称 Returns: Dict[str, Any]: 库位详情信息 """ try: # 查询库位 layer = db.query(OperatePointLayer).filter( OperatePointLayer.layer_name == layer_name, OperatePointLayer.is_deleted == False ).first() if not layer: raise ValueError(f"库位 {layer_name} 不存在") # 获取库位详细信息 storage_location_info = OperatePointService._convert_to_storage_location_info( layer, include_operate_point_info=True, include_extended_fields=True ) # 获取动作点详细信息 operate_point_info = None if layer.operate_point: operate_point_info = { "id": layer.operate_point.id, "station_name": layer.operate_point.station_name, "storage_location_name": layer.operate_point.storage_location_name, "scene_id": layer.operate_point.scene_id, "storage_area_id": layer.operate_point.storage_area_id, "storage_area_type": layer.operate_point.storage_area_type.value if layer.operate_point.storage_area_type else None, "area_name": layer.operate_point.area_name, "max_layers": layer.operate_point.max_layers, "current_layers": layer.operate_point.current_layers, "position_x": layer.operate_point.position_x, "position_y": layer.operate_point.position_y, "position_z": layer.operate_point.position_z, "description": layer.operate_point.description, "created_at": layer.operate_point.created_at, "updated_at": layer.operate_point.updated_at, "is_deleted": layer.operate_point.is_deleted } # 获取扩展字段定义 extended_fields_definitions = [] if layer.config_json: try: import json config = json.loads(layer.config_json) if 'extended_fields' in config: # 获取所有扩展属性定义 extended_properties = db.query(ExtendedProperty).filter( ExtendedProperty.is_deleted == False, ExtendedProperty.is_enabled == True ).all() for prop in extended_properties: extended_fields_definitions.append( OperatePointService._convert_to_extended_property_info(prop) ) except Exception as e: logger.error(f"解析扩展字段定义失败: {str(e)}") # 构建状态变更历史(暂时为空,后续可以扩展) status_history = [] return { "storage_location": storage_location_info, "operate_point_info": operate_point_info, "extended_fields_definitions": extended_fields_definitions, "status_history": status_history } except Exception as e: logger.error(f"获取库位详情失败: {str(e)}") raise @staticmethod def edit_storage_location(db: Session, layer_name: str, request: Any) -> Dict[str, Any]: """ 编辑库位信息 Args: db: 数据库会话 layer_name: 库位名称 request: 库位编辑请求 Returns: Dict[str, Any]: 编辑响应 """ try: # 查询库位 layer = db.query(OperatePointLayer).filter( OperatePointLayer.layer_name == layer_name, OperatePointLayer.is_deleted == False ).first() if not layer: raise ValueError(f"库位 {layer_name} 不存在") # 跟踪更新的字段 updated_fields = [] # 更新基本字段 if request.goods_content is not None and request.goods_content != layer.goods_content: layer.goods_content = request.goods_content updated_fields.append("goods_content") if request.goods_weight is not None and request.goods_weight != layer.goods_weight: layer.goods_weight = request.goods_weight updated_fields.append("goods_weight") if request.goods_volume is not None and request.goods_volume != layer.goods_volume: layer.goods_volume = request.goods_volume updated_fields.append("goods_volume") if request.max_weight is not None and request.max_weight != layer.max_weight: layer.max_weight = request.max_weight updated_fields.append("max_weight") if request.max_volume is not None and request.max_volume != layer.max_volume: layer.max_volume = request.max_volume updated_fields.append("max_volume") if request.layer_height is not None and request.layer_height != layer.layer_height: layer.layer_height = request.layer_height updated_fields.append("layer_height") # 更新状态字段 if request.is_locked is not None and request.is_locked != layer.is_locked: layer.is_locked = request.is_locked updated_fields.append("is_locked") if request.is_disabled is not None and request.is_disabled != layer.is_disabled: layer.is_disabled = request.is_disabled updated_fields.append("is_disabled") if request.is_empty_tray is not None and request.is_empty_tray != layer.is_empty_tray: layer.is_empty_tray = request.is_empty_tray updated_fields.append("is_empty_tray") if request.tags is not None and request.tags != layer.tags: layer.tags = request.tags updated_fields.append("tags") if request.description is not None and request.description != layer.description: layer.description = request.description updated_fields.append("description") # 更新扩展字段 if request.extended_fields is not None: # 解析现有的config_json config = {} if layer.config_json: try: import json 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 field_name, field_value in request.extended_fields.items(): if field_name in config['extended_fields']: # 检查值是否真正发生了变化 current_value = config['extended_fields'][field_name].get('value') if current_value != field_value: config['extended_fields'][field_name]['value'] = field_value config['extended_fields'][field_name]['updated_at'] = datetime.datetime.now().isoformat() updated_fields.append(f"extended_fields.{field_name}") else: # 新增扩展字段(需要验证是否存在定义) extended_property = db.query(ExtendedProperty).filter( ExtendedProperty.property_name == field_name, ExtendedProperty.is_deleted == False, ExtendedProperty.is_enabled == True ).first() if extended_property: config['extended_fields'][field_name] = { 'value': field_value, 'type': extended_property.property_type.value, 'is_required': extended_property.is_required, 'updated_at': datetime.datetime.now().isoformat() } updated_fields.append(f"extended_fields.{field_name}") else: logger.warning(f"扩展字段 {field_name} 不存在或已禁用,跳过更新") # 只有当扩展字段确实发生了变化时才更新config_json if any(field.startswith("extended_fields.") for field in updated_fields): try: import json layer.config_json = json.dumps(config, ensure_ascii=False, indent=2) except Exception as e: logger.error(f"序列化库位层 {layer.id} 的config_json失败: {str(e)}") raise ValueError(f"更新扩展字段失败: {str(e)}") # 检查是否有实际的字段变更 if not updated_fields: return { "layer_name": layer_name, "success": True, "message": "没有字段发生变化,数据保持不变", "updated_fields": [], "updated_storage_location": OperatePointService._convert_to_storage_location_info( layer, include_operate_point_info=True, include_extended_fields=True ) } # 更新最后修改时间 layer.updated_at = datetime.datetime.now() updated_fields.append("updated_at") # 提交更改 db.commit() # 记录操作日志 try: operator = OperatePointService._get_operator() OperatePointService.create_storage_location_log( db=db, operator=operator, operation_type="编辑库位", affected_storage_locations=[layer_name], description=f"编辑库位信息,更新字段: {', '.join(updated_fields)}" ) except Exception as e: logger.error(f"记录操作日志失败: {str(e)}") # 获取更新后的库位信息 updated_storage_location = OperatePointService._convert_to_storage_location_info( layer, include_operate_point_info=True, include_extended_fields=True ) return { "layer_name": layer_name, "success": True, "message": f"库位信息更新成功,共更新 {len(updated_fields)} 个字段", "updated_fields": updated_fields, "updated_storage_location": updated_storage_location } except Exception as e: logger.error(f"编辑库位信息失败: {str(e)}") db.rollback() raise @staticmethod def create_storage_location_log( db: Session, operator: str, operation_type: str, affected_storage_locations: List[str], description: Optional[str] = None ) -> StorageLocationLog: """ 创建库位操作记录 Args: db: 数据库会话 operator: 操作人 operation_type: 操作类型 affected_storage_locations: 影响的库位ID列表 description: 操作描述 Returns: StorageLocationLog: 创建的操作记录 """ try: import json # 创建操作记录 log = StorageLocationLog( operator=operator, operation_type=operation_type, affected_storage_locations=json.dumps(affected_storage_locations, ensure_ascii=False), description=description ) db.add(log) db.commit() return log except Exception as e: logger.error(f"创建库位操作记录失败: {str(e)}") db.rollback() raise @staticmethod def get_storage_location_logs(db: Session, request: StorageLocationLogListRequest) -> StorageLocationLogListResponse: """ 获取库位操作记录列表 Args: db: 数据库会话 request: 操作记录查询请求 Returns: StorageLocationLogListResponse: 操作记录列表响应 """ try: # 构建查询条件 query = db.query(StorageLocationLog).filter(StorageLocationLog.is_deleted == False) # 根据库位ID筛选 if request.storage_location_id: query = query.filter(StorageLocationLog.affected_storage_locations.like(f'%"{request.storage_location_id}"%')) # 根据操作人筛选(模糊搜索) if request.operator: query = query.filter(StorageLocationLog.operator.like(f"%{request.operator}%")) # 根据操作类型筛选 if request.operation_type: query = query.filter(StorageLocationLog.operation_type == request.operation_type) # 根据时间范围筛选 if request.start_time: query = query.filter(StorageLocationLog.operation_time >= request.start_time) if request.end_time: query = query.filter(StorageLocationLog.operation_time <= request.end_time) # 按操作时间倒序排列 query = query.order_by(StorageLocationLog.operation_time.desc()) # 获取总数 total = query.count() # 分页 offset = (request.page - 1) * request.page_size logs = query.offset(offset).limit(request.page_size).all() # 转换为响应对象 log_infos = [] for log in logs: log_info = OperatePointService._convert_to_storage_location_log_info(log) log_infos.append(log_info) # 计算总页数 total_pages = math.ceil(total / request.page_size) if total > 0 else 0 return StorageLocationLogListResponse( total=total, page=request.page, page_size=request.page_size, total_pages=total_pages, logs=log_infos ) except Exception as e: logger.error(f"获取库位操作记录列表失败: {str(e)}") raise @staticmethod def _convert_to_storage_location_log_info(log: StorageLocationLog) -> StorageLocationLogInfo: """ 将数据库模型转换为库位操作记录信息响应对象 Args: log: 库位操作记录数据库模型 Returns: StorageLocationLogInfo: 操作记录信息 """ try: import json # 解析影响的库位ID列表 affected_storage_locations = [] if log.affected_storage_locations: try: affected_storage_locations = json.loads(log.affected_storage_locations) except Exception as e: logger.error(f"解析影响库位列表失败: {str(e)}") affected_storage_locations = [] return StorageLocationLogInfo( id=str(log.id), operation_time=log.operation_time, operator=log.operator, operation_type=log.operation_type, affected_storage_locations=affected_storage_locations, description=log.description, created_at=log.created_at ) except Exception as e: logger.error(f"转换库位操作记录信息失败: {str(e)}") raise