#!/usr/bin/env python # -*- coding: utf-8 -*- """ 库位管理API路由 实现库位的管理功能 """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import Any, Dict, Optional from data.session import get_db from services.operate_point_service import OperatePointService from routes.model.base import ApiResponse from routes.model.operate_point_model import ( # OperatePointListRequest, OperatePointListResponse, StorageLocationListRequest, StorageLocationListResponse, StorageLocationStatusUpdateRequest, StorageLocationStatusUpdateResponse, BatchStorageLocationStatusUpdateRequest, BatchStorageLocationStatusUpdateResponse, StorageLocationActionEnum, # StorageAreaTypeEnum ExtendedPropertyCreateRequest, ExtendedPropertyCreateResponse, ExtendedPropertyListRequest, ExtendedPropertyListResponse, ExtendedPropertyDeleteResponse, StorageLocationDetailResponse, StorageLocationEditRequest, StorageLocationEditResponse, StorageLocationLogListRequest, StorageLocationLogListResponse, ) from routes.common_api import format_response, error_response from utils.logger import get_logger from data.models import OperatePointLayer from data.models import OperatePoint # Added missing import # 创建路由 router = APIRouter(prefix="/api/vwed-operate-point", tags=["动作点管理"]) # 设置日志 logger = get_logger("app.operate_point_api") def get_action_descriptions(): """获取操作类型的说明文档""" descriptions = { StorageLocationActionEnum.OCCUPY: "占用库位", StorageLocationActionEnum.RELEASE: "释放库位", StorageLocationActionEnum.LOCK: "锁定库位(需要提供锁定者)", StorageLocationActionEnum.UNLOCK: "解锁库位", StorageLocationActionEnum.ENABLE: "启用库位", StorageLocationActionEnum.DISABLE: "禁用库位", StorageLocationActionEnum.SET_EMPTY_TRAY: "设置为空托盘", StorageLocationActionEnum.CLEAR_EMPTY_TRAY: "清除空托盘状态" } return descriptions # 标准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.get("/list", response_model=ApiResponse[StorageLocationListResponse]) async def get_storage_location_list( scene_id: Optional[str] = Query(None, description="场景ID"), storage_area_id: Optional[str] = Query(None, description="库区ID"), station_name: Optional[str] = Query(None, description="站点名称(支持模糊搜索)"), layer_name: Optional[str] = Query(None, description="层名称(支持模糊搜索)"), is_disabled: Optional[bool] = Query(None, description="是否禁用"), is_occupied: Optional[bool] = Query(None, description="是否占用"), is_locked: Optional[bool] = Query(None, description="是否锁定"), is_empty_tray: Optional[bool] = Query(None, description="是否空托盘"), include_operate_point_info: bool = Query(True, description="是否包含动作点信息"), include_extended_fields: bool = Query(True, description="是否包含扩展字段"), page: int = Query(1, ge=1, description="页码"), page_size: int = Query(20, ge=1, le=100, description="每页数量"), db: Session = Depends(get_db) ): """ 获取库位列表 库位基于动作点分层(OperatePointLayer),每一层对应一个库位。 支持多种筛选条件: - 场景ID:根据场景筛选库位 - 库区ID:根据库区筛选库位 - 站点名称:支持模糊搜索 - 层名称:支持模糊搜索 - 是否禁用:筛选禁用/启用的库位 - 是否占用:筛选已占用/空闲的库位 - 是否锁定:筛选锁定/解锁的库位 - 是否空托盘:筛选空托盘/非空托盘的库位 - 是否包含动作点信息:控制返回数据是否包含所属动作点的详细信息 - 是否包含扩展字段:控制返回数据是否包含自定义扩展字段 返回数据包含: - 库位基本信息(ID、层索引、层名称等) - 库位状态(是否占用、锁定、禁用、空托盘等) - 货物信息(货物内容、重量、体积等) - 库位规格(最大承重、最大体积、层高等) - 动作点信息(如果启用include_operate_point_info) - 扩展字段(如果启用include_extended_fields) - 统计信息(总数、各种状态的数量、使用率等) Args: scene_id: 场景ID storage_area_id: 库区ID station_name: 站点名称 layer_name: 层名称 is_disabled: 是否禁用 is_occupied: 是否占用 is_locked: 是否锁定 is_empty_tray: 是否空托盘 include_operate_point_info: 是否包含动作点信息 include_extended_fields: 是否包含扩展字段 page: 页码 page_size: 每页数量 db: 数据库会话 Returns: ApiResponse[StorageLocationListResponse]: 库位列表响应 """ try: # 构建请求对象 request = StorageLocationListRequest( scene_id=scene_id, storage_area_id=storage_area_id, station_name=station_name, layer_name=layer_name, is_disabled=is_disabled, is_occupied=is_occupied, is_locked=is_locked, is_empty_tray=is_empty_tray, include_operate_point_info=include_operate_point_info, include_extended_fields=include_extended_fields, page=page, page_size=page_size ) # 调用服务层方法获取库位列表 result = OperatePointService.get_storage_location_list(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.put("/status", response_model=ApiResponse[StorageLocationStatusUpdateResponse]) async def update_storage_location_status( request: StorageLocationStatusUpdateRequest, db: Session = Depends(get_db) ): """ 更新库位状态 支持的操作类型: - occupy: 占用库位 - release: 释放库位 - lock: 锁定库位(需要提供锁定者) - unlock: 解锁库位 - enable: 启用库位 - disable: 禁用库位 - set_empty_tray: 设置为空托盘 - clear_empty_tray: 清除空托盘状态 Args: request: 库位状态更新请求 db: 数据库会话 Returns: ApiResponse[StorageLocationStatusUpdateResponse]: 状态更新响应 """ try: # 验证操作类型 - 使用枚举类型 valid_actions = [action.value for action in StorageLocationActionEnum] if request.action not in valid_actions: return error_response(f"不支持的操作类型: {request.action},支持的操作:{', '.join(valid_actions)}", 400) # 锁定操作必须提供锁定者 if request.action == StorageLocationActionEnum.LOCK and not request.locked_by: return error_response("锁定操作必须提供锁定者", 400) # 调用服务层方法 result = OperatePointService.update_storage_location_status(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.put("/batch-status", response_model=ApiResponse[BatchStorageLocationStatusUpdateResponse]) async def batch_update_storage_location_status( request: BatchStorageLocationStatusUpdateRequest, db: Session = Depends(get_db) ): """ 批量更新库位状态 支持的操作类型: - occupy: 占用库位 - release: 释放库位 - lock: 锁定库位(需要提供锁定者) - unlock: 解锁库位 - enable: 启用库位 - disable: 禁用库位 - set_empty_tray: 设置为空托盘 - clear_empty_tray: 清除空托盘状态 Args: request: 批量库位状态更新请求 db: 数据库会话 Returns: ApiResponse[BatchStorageLocationStatusUpdateResponse]: 批量状态更新响应 """ try: # 验证操作类型 - 使用枚举类型 valid_actions = [action.value for action in StorageLocationActionEnum] if request.action not in valid_actions: return error_response(f"不支持的操作类型: {request.action},支持的操作:{', '.join(valid_actions)}", 400) # 锁定操作必须提供锁定者 if request.action == StorageLocationActionEnum.LOCK and not request.locked_by: return error_response("锁定操作必须提供锁定者", 400) # 验证库位名称列表 if not request.layer_names: return error_response("库位名称列表不能为空", 400) if len(request.layer_names) > 100: return error_response("批量操作的库位数量不能超过100个", 400) # 调用服务层方法 result = OperatePointService.batch_update_storage_location_status(db=db, request=request) # 构建更详细的响应消息 success_details = [] if result.success_count > 0: success_details.append(f"成功操作 {result.success_count} 个库位") if result.failed_count > 0: success_details.append(f"失败操作 {result.failed_count} 个库位") # 统计无需更改的操作 no_change_count = sum(1 for r in result.results if r.success and "无需重复操作" in r.message) actual_update_count = result.success_count - no_change_count if no_change_count > 0: success_details.append(f"其中 {no_change_count} 个库位已是目标状态") if actual_update_count > 0: success_details.append(f"实际更新 {actual_update_count} 个库位") detailed_message = f"批量状态更新完成:{', '.join(success_details)}" return api_response(message=detailed_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.get("/actions") async def get_supported_actions(): """ 获取支持的操作类型列表 Returns: ApiResponse: 支持的操作类型列表及其说明 """ try: descriptions = get_action_descriptions() actions = [] for action_enum in StorageLocationActionEnum: actions.append({ "value": action_enum.value, "description": descriptions.get(action_enum, "") }) return api_response( message="获取支持的操作类型成功", data={ "actions": actions, "count": len(actions) } ) except Exception as e: logger.error(f"获取支持的操作类型失败: {str(e)}") return error_response(f"获取支持的操作类型失败: {str(e)}", 500) @router.get("/{layer_name}/status") async def get_storage_location_status( layer_name: str, db: Session = Depends(get_db) ): """ 获取单个库位状态信息 Args: layer_name: 库位名称 db: 数据库会话 Returns: ApiResponse: 库位状态信息 """ try: # 查询库位 storage_location = db.query(OperatePointLayer).filter( OperatePointLayer.layer_name == layer_name, OperatePointLayer.is_deleted == False ).first() if not storage_location: return error_response("库位不存在", 404) # 获取状态信息 status = OperatePointService._get_storage_location_status(storage_location) return api_response(message="查询成功", data=status) except Exception as e: logger.error(f"获取库位状态失败: {str(e)}") return error_response(f"获取库位状态失败: {str(e)}", 500) # 扩展属性管理接口 @router.post("/extended-properties", response_model=ApiResponse[ExtendedPropertyCreateResponse]) async def create_extended_property( request: ExtendedPropertyCreateRequest, db: Session = Depends(get_db) ): """ 创建扩展属性 用于创建新的扩展属性定义,这些属性可以在库位管理中使用。 重要提示: - 创建扩展属性后,会自动将该属性添加到所有现有的库位层中 - 每个库位层的config_json会自动更新,包含新的扩展属性配置 - 新属性会使用指定的默认值进行初始化 支持的属性类型: - string: 字符串 - integer: 整数 - float: 浮点数 - boolean: 布尔值 - date: 日期 - datetime: 日期时间 - text: 长文本 - select: 下拉选择 - multiselect: 多选 Args: request: 扩展属性创建请求 db: 数据库会话 Returns: ApiResponse[ExtendedPropertyCreateResponse]: 创建响应 """ try: # 调用服务层方法 result = OperatePointService.create_extended_property(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.get("/extended-properties", response_model=ApiResponse[ExtendedPropertyListResponse]) async def get_extended_property_list( property_name: Optional[str] = Query(None, description="属性名称(支持模糊搜索)"), property_type: Optional[str] = Query(None, description="属性类型"), category: Optional[str] = Query(None, description="属性分类"), is_enabled: Optional[bool] = Query(None, description="是否启用"), page: int = Query(1, ge=1, description="页码"), page_size: int = Query(20, ge=1, le=100, description="每页数量"), db: Session = Depends(get_db) ): """ 获取扩展属性列表 支持多种筛选条件: - 属性名称:支持模糊搜索 - 属性类型:精确匹配 - 属性分类:精确匹配 - 是否启用:筛选启用/禁用的属性 返回数据包含: - 属性基本信息(ID、名称、类型等) - 属性设置(是否必填、是否启用等) - 验证规则和选项配置 - 显示设置(宽度、格式等) - 创建和更新时间 Args: property_name: 属性名称 property_type: 属性类型 category: 属性分类 is_enabled: 是否启用 page: 页码 page_size: 每页数量 db: 数据库会话 Returns: ApiResponse[ExtendedPropertyListResponse]: 属性列表响应 """ try: # 构建请求对象 request = ExtendedPropertyListRequest( property_name=property_name, property_type=property_type, category=category, is_enabled=is_enabled, page=page, page_size=page_size ) # 调用服务层方法 result = OperatePointService.get_extended_property_list(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("/extended-properties/{property_id}", response_model=ApiResponse[ExtendedPropertyDeleteResponse]) async def delete_extended_property( property_id: str, db: Session = Depends(get_db) ): """ 删除扩展属性 删除指定的扩展属性(软删除)。 重要提示: - 删除扩展属性后,会自动从所有现有的库位层中移除该属性 - 每个库位层的config_json会自动更新,清除已删除的扩展属性配置 - 此操作会影响所有库位的扩展属性数据 注意:此操作不可逆,请谨慎使用。 Args: property_id: 属性ID db: 数据库会话 Returns: ApiResponse[ExtendedPropertyDeleteResponse]: 删除响应 """ try: # 调用服务层方法 result = OperatePointService.delete_extended_property(db=db, property_id=property_id) 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.get("/extended-properties/types") # async def get_extended_property_types(): # """ # 获取支持的扩展属性类型列表 # 返回系统支持的所有扩展属性类型及其说明。 # Returns: # ApiResponse: 支持的属性类型列表 # """ # try: # from data.models.extended_property import ExtendedPropertyTypeEnum # # 属性类型说明 # type_descriptions = { # ExtendedPropertyTypeEnum.STRING: "字符串 - 适用于短文本输入", # ExtendedPropertyTypeEnum.INTEGER: "整数 - 适用于整数值", # ExtendedPropertyTypeEnum.FLOAT: "浮点数 - 适用于小数值", # ExtendedPropertyTypeEnum.BOOLEAN: "布尔值 - 适用于是/否选择", # ExtendedPropertyTypeEnum.DATE: "日期 - 适用于日期选择", # ExtendedPropertyTypeEnum.DATETIME: "日期时间 - 适用于日期和时间选择", # ExtendedPropertyTypeEnum.TEXT: "长文本 - 适用于多行文本输入", # ExtendedPropertyTypeEnum.SELECT: "下拉选择 - 适用于单选择", # ExtendedPropertyTypeEnum.MULTISELECT: "多选 - 适用于多选择" # } # types = [] # for type_enum in ExtendedPropertyTypeEnum: # types.append({ # "value": type_enum.value, # "description": type_descriptions.get(type_enum, "") # }) # return api_response( # message="获取扩展属性类型成功", # data={ # "types": types, # "count": len(types) # } # ) # except Exception as e: # logger.error(f"获取扩展属性类型失败: {str(e)}") # return error_response(f"获取扩展属性类型失败: {str(e)}", 500) @router.get("/operation-logs", response_model=ApiResponse[StorageLocationLogListResponse]) async def get_storage_location_operation_logs( layer_name: Optional[str] = Query(None, description="库位名称"), operator: Optional[str] = Query(None, description="操作人(支持模糊搜索)"), operation_type: Optional[str] = Query(None, description="操作类型"), start_time: Optional[str] = Query(None, description="开始时间 (格式: YYYY-MM-DD HH:MM:SS)"), end_time: Optional[str] = Query(None, description="结束时间 (格式: YYYY-MM-DD HH:MM:SS)"), page: int = Query(1, ge=1, description="页码"), page_size: int = Query(20, ge=1, le=100, description="每页数量"), db: Session = Depends(get_db) ): """ 获取库位操作记录列表 获取库位相关的操作记录,包括: - 状态更新操作(占用、释放、锁定、解锁、启用、禁用等) - 库位信息编辑操作 - 其他库位相关的操作 支持多种筛选条件: - 库位名称:查询特定库位的操作记录 - 操作人:支持模糊搜索操作人姓名 - 操作类型:筛选特定类型的操作 - 时间范围:指定操作时间的开始和结束时间 操作记录按时间倒序排列(最新的操作在前)。 Args: layer_name: 库位名称 operator: 操作人 operation_type: 操作类型 start_time: 开始时间 end_time: 结束时间 page: 页码 page_size: 每页数量 db: 数据库会话 Returns: ApiResponse[StorageLocationLogListResponse]: 操作记录列表响应 """ try: # 时间格式转换 start_time_dt = None end_time_dt = None if start_time: try: from datetime import datetime start_time_dt = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") except ValueError: return error_response("开始时间格式错误,请使用格式:YYYY-MM-DD HH:MM:SS", 400) if end_time: try: from datetime import datetime end_time_dt = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S") except ValueError: return error_response("结束时间格式错误,请使用格式:YYYY-MM-DD HH:MM:SS", 400) # 验证时间范围 if start_time_dt and end_time_dt and start_time_dt > end_time_dt: return error_response("开始时间不能大于结束时间", 400) # 构建请求对象 request = StorageLocationLogListRequest( layer_name=layer_name, operator=operator, operation_type=operation_type, start_time=start_time_dt, end_time=end_time_dt, page=page, page_size=page_size ) # 调用服务层方法 result = OperatePointService.get_storage_location_logs(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.get("/{layer_name}", response_model=ApiResponse[StorageLocationDetailResponse]) async def get_storage_location_detail( layer_name: str, db: Session = Depends(get_db) ): """ 获取库位详情 获取指定库位的详细信息,包括: - 库位基本信息和当前状态 - 动作点详细信息 - 扩展字段定义和值 - 状态变更历史记录 Args: layer_name: 库位名称 db: 数据库会话 Returns: ApiResponse[StorageLocationDetailResponse]: 库位详情响应 """ try: # 调用服务层方法 result = OperatePointService.get_storage_location_detail(db=db, layer_name=layer_name) return api_response(message="获取库位详情成功", data=result) except ValueError as e: return error_response(str(e), 404) except Exception as e: logger.error(f"获取库位详情失败: {str(e)}") return error_response(f"获取库位详情失败: {str(e)}", 500) @router.put("/{layer_name}", response_model=ApiResponse[StorageLocationEditResponse]) async def edit_storage_location( layer_name: str, request: StorageLocationEditRequest, db: Session = Depends(get_db) ): """ 编辑库位信息 允许修改库位的各种属性,包括: - 货物信息:货物内容、重量、体积等 - 库位规格:最大承重、最大体积、层高等 - 状态字段:是否锁定、是否禁用、是否空托盘 - 扩展字段:自定义的扩展属性值 - 其他属性:标签、描述等 注意: - 只有传入的字段且值发生变化时才会被更新 - 层名称(layer_name)不能通过此接口修改 - 扩展字段必须在系统中已定义且已启用 - 如果所有字段都没有发生变化,会返回相应提示信息 Args: layer_name: 库位名称 request: 库位编辑请求 db: 数据库会话 Returns: ApiResponse[StorageLocationEditResponse]: 编辑响应 """ try: # 调用服务层方法 result = OperatePointService.edit_storage_location( db=db, layer_name=layer_name, 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)