VWED_server/routes/operate_point_api.py
2025-07-17 15:47:56 +08:00

721 lines
25 KiB
Python
Raw Permalink 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 -*-
"""
库位管理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)