完善库位库区逻辑

This commit is contained in:
靳中伟 2025-07-15 15:05:04 +08:00
parent 2899453300
commit d8c31ebd5b
23 changed files with 23356 additions and 2002 deletions

View File

@ -4,10 +4,11 @@
地图数据推送接口用于处理用户推送新地图时的动作点和库区数据存储。支持库区分类(密集库区、一般库区)和动作点分层存储功能。
**重要特性:** 采用智能覆盖模式,先查询数据库是否有相关场景数据:
- 如果有现有数据,则先删除现有数据,然后创建新数据
- 如果没有现有数据,则直接创建新数据
- 确保数据的完整性和一致性
**重要特性:** 采用增量更新模式,在原有数据基础上进行更新:
- 库区基于area_name判断存在则更新不存在则新增
- 动作点基于station_name判断存在则更新不存在则新增
- 分层基于layer_name判断存在则更新不存在则新增
- 确保数据的完整性和一致性,避免数据丢失风险
- 支持动作站点名称和库位名称的重复检查,自动过滤重复数据
## 接口列表
@ -18,7 +19,7 @@
**功能说明:** 当用户推送新的地图时,将地图相关的动作点和库区数据存入数据库。支持库区分类和动作点分层存储。每个动作点包含站点名称和库位名称两个唯一标识。
**重要说明:** 采用智能覆盖模式,先查询数据库是否有相关场景数据,如果有则先删除现有数据再创建,如果没有则直接创建。自动检查并过滤重复的动作点
**重要说明:** 采用增量更新模式在原有数据基础上进行更新。库区通过area_name判断动作点通过station_name判断分层通过layer_name判断。系统会自动生成ID和layer_index确保数据安全
**请求参数:**
@ -32,13 +33,14 @@
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| id | string | 是 | 库区ID |
| area_name | string | 是 | 库区名称 |
| area_code | string | 是 | 库区编码 |
| area_type | string | 是 | 库区类型general-一般库区dense-密集库区) |
| select_logic | integer | 是 | 选择库位逻辑1-先进先出2-先进后出) |
| description | string | 否 | 库区描述 |
| tags | string | 否 | 库区标签 |
**注意:** 库区ID由系统自动生成UUID无需手动指定。
**注意:** 库区的最大容量max_capacity由系统根据库区类型和包含的动作点数量自动计算无需手动指定。
**动作点数据结构operate_points**
@ -47,7 +49,7 @@
|--------|------|------|------|
| station_name | string | 是 | 动作站点名称(唯一标识) |
| storage_location_name | string | 是 | 库位名称(唯一标识) |
| storage_area_id | string | 否 | 所属库区ID |
| area_name | string | 否 | 所属库区名称 |
| max_layers | integer | 否 | 最大层数默认1 |
| position_x | integer | 否 | X坐标 |
| position_y | integer | 否 | Y坐标 |
@ -57,13 +59,12 @@
| description | string | 否 | 动作点描述 |
| layers | array | 否 | 分层数据 |
**注意:** 动作点ID由系统自动生成UUID无需手动指定。站点名称和库位名称作为唯一标识同一场景下不能重复。
**注意:** 动作点ID由系统自动生成UUID无需手动指定。站点名称和库位名称作为唯一标识同一场景下不能重复。动作点通过area_name字段与库区关联。
**分层数据结构layers**
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| layer_index | integer | 是 | 层索引从1开始 |
| layer_name | string | 否 | 层名称 |
| max_weight | integer | 否 | 最大承重(克) |
| max_volume | integer | 否 | 最大体积(立方厘米) |
@ -71,45 +72,65 @@
| description | string | 否 | 层描述 |
| tags | string | 否 | 层标签 |
**注意:** 层索引layer_index由系统自动生成按照提交的顺序从1开始递增。
**请求示例:**
```json
{
"scene_id": "scene-001",
"scene_id": "1936411520272753371",
"storage_areas": [
{
"id": "area-002",
"area_name": "一般存储区B",
"area_code": "GENERAL-B",
"area_type": "general"
"area_type": "general",
"select_logic": 2 //1 先进先出 2 先进后出
},
{
"area_name": "一般存储区C",
"area_type": "general",
"select_logic": 2 //1 先进先出 2 先进后出
}
],
"operate_points": [
{
"station_name": "STATION-A-001",
"storage_location_name": "库位A001",
"storage_area_id": "area-002",
"area_name": "一般存储区B",
"max_layers": 2,
"layers": [
{
"layer_index": 1,
"layer_name": "第1层"
"layer_name": "1-1"
},
{
"layer_index": 2,
"layer_name": "第2层"
"layer_name": "1-2"
}
]
},
{
"station_name": "STATION-B-001",
"storage_location_name": "库位B002",
"storage_area_id": "area-002",
"area_name": "一般存储区B",
"max_layers": 1,
"layers": [
{
"layer_index": 1,
"layer_name": "第1层"
"layer_name": "2-1"
}
]
},
{
"station_name": "STATION-B-004",
"storage_location_name": "库位B004",
"area_name": "一般存储区C",
"max_layers": 3,
"layers": [
{
"layer_name": "4-1"
},
{
"layer_name": "4-2"
},
{
"layer_name": "4-3"
}
]
},
@ -119,8 +140,7 @@
"max_layers": 1,
"layers": [
{
"layer_index": 1,
"layer_name": "第1层"
"layer_name": "3-1"
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -239,7 +239,7 @@
"label": "是否为降序",
"description": "",
"required": false,
"defaultValue": true,
"defaultValue": false,
"options": []
}
],

View File

@ -24,7 +24,6 @@ class OperatePointLayer(BaseModel):
operate_point_id = Column(CHAR(64), ForeignKey('vwed_operate_point.id'), nullable=False, comment='动作点ID')
station_name = Column(String(64), nullable=False, comment='动作点名称')
storage_location_name = Column(String(64), nullable=False, comment='库位名称')
area_id = Column(CHAR(32), nullable=True, comment='库区ID冗余字段')
area_name = Column(String(64), nullable=True, comment='库区名称')
scene_id = Column(String(64), nullable=False, comment='场景ID冗余字段')
layer_index = Column(Integer, nullable=False, comment='层索引(从1开始)')
@ -67,7 +66,7 @@ class OperatePointLayer(BaseModel):
)
def __repr__(self):
return f"<OperatePointLayer(id={self.id}, operate_point_id={self.operate_point_id}, station_name={self.station_name}, storage_location_name={self.storage_location_name}, area_id={self.area_id}, area_name={self.area_name}, scene_id={self.scene_id}, layer_index={self.layer_index})>"
return f"<OperatePointLayer(id={self.id}, operate_point_id={self.operate_point_id}, station_name={self.station_name}, storage_location_name={self.storage_location_name}, area_name={self.area_name}, scene_id={self.scene_id}, layer_index={self.layer_index})>"
def can_store_goods(self, weight=None, volume=None):
"""

View File

@ -26,12 +26,12 @@ class StorageArea(BaseModel):
"""
__tablename__ = 'vwed_storage_area'
id = Column(CHAR(32), primary_key=True, comment='库区ID')
id = Column(CHAR(64), primary_key=True, comment='库区ID')
area_name = Column(String(64), nullable=False, unique=True, comment='库区名称')
area_code = Column(String(32), nullable=False, unique=True, comment='库区编码')
area_type = Column(Enum(StorageAreaType), nullable=False, default=StorageAreaType.GENERAL, comment='库区类型')
scene_id = Column(String(32), nullable=False, comment='场景ID')
# 选择库位逻辑 1 先进先出 2 先进后出
select_logic = Column(Integer, nullable=False, default=1, comment='选择库位逻辑 1 先进先出 2 先进后出 ')
# 库区属性
max_capacity = Column(Integer, nullable=False, default=0, comment='最大容量')
current_usage = Column(Integer, nullable=False, default=0, comment='当前使用量')

21149
logs/app.log

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,6 @@ class StorageAreaTypeEnum(str, Enum):
class OperatePointLayerData(BaseModel):
"""动作点分层数据"""
layer_index: int = Field(..., ge=1, description="层索引(从1开始)")
layer_name: Optional[str] = Field(None, description="层名称")
max_weight: Optional[int] = Field(None, ge=0, description="最大承重(克)")
max_volume: Optional[int] = Field(None, ge=0, description="最大体积(立方厘米)")
@ -32,7 +31,7 @@ class OperatePointData(BaseModel):
"""动作点数据"""
station_name: str = Field(..., description="动作站点名称")
storage_location_name: str = Field(..., description="库位名称")
storage_area_id: Optional[str] = Field(None, description="所属库区ID")
area_name: Optional[str] = Field(None, description="所属库区名称")
max_layers: int = Field(1, ge=1, description="最大层数")
position_x: Optional[int] = Field(None, description="X坐标")
position_y: Optional[int] = Field(None, description="Y坐标")
@ -52,28 +51,22 @@ class OperatePointData(BaseModel):
if len(v) > max_layers:
raise ValueError(f"分层数量({len(v)})不能超过最大层数({max_layers})")
# 检查层索引是否重复
layer_indices = [layer.layer_index for layer in v]
if len(layer_indices) != len(set(layer_indices)):
raise ValueError("层索引不能重复")
# 检查层索引是否超出范围
for layer in v:
if layer.layer_index > max_layers:
raise ValueError(f"层索引({layer.layer_index})不能超过最大层数({max_layers})")
# 检查层名称是否重复
if v:
layer_names = [layer.layer_name for layer in v if layer.layer_name]
if len(layer_names) != len(set(layer_names)):
raise ValueError("层名称不能重复")
return v
class StorageAreaData(BaseModel):
"""库区数据"""
id: str = Field(..., description="库区ID")
area_name: str = Field(..., description="库区名称")
area_code: str = Field(..., description="库区编码")
area_type: StorageAreaTypeEnum = Field(StorageAreaTypeEnum.GENERAL, description="库区类型")
description: Optional[str] = Field(None, description="库区描述")
tags: Optional[str] = Field(None, description="库区标签")
select_logic: int = Field(..., description="选择库位逻辑 1 先进先出 2 先进后出 ")
class MapDataPushRequest(BaseModel):
"""地图数据推送请求"""
@ -92,13 +85,10 @@ class MapDataPushRequest(BaseModel):
if len(station_names) != len(set(station_names)):
raise ValueError("动作站点名称不能重复")
# 检查库区ID是否存在
storage_areas = values.get('storage_areas', [])
storage_area_ids = {area.id for area in storage_areas}
for point in v:
if point.storage_area_id and point.storage_area_id not in storage_area_ids:
raise ValueError(f"动作点'{point.station_name}'的库区ID'{point.storage_area_id}'不存在")
# 检查库位名称是否重复
storage_location_names = [point.storage_location_name for point in v]
if len(storage_location_names) != len(set(storage_location_names)):
raise ValueError("库位名称不能重复")
return v
@ -108,21 +98,11 @@ class MapDataPushRequest(BaseModel):
if not v:
return v
# 检查库区ID是否重复
area_ids = [area.id for area in v]
if len(area_ids) != len(set(area_ids)):
raise ValueError("库区ID不能重复")
# 检查库区名称是否重复
area_names = [area.area_name for area in v]
if len(area_names) != len(set(area_names)):
raise ValueError("库区名称不能重复")
# 检查库区编码是否重复
area_codes = [area.area_code for area in v]
if len(area_codes) != len(set(area_codes)):
raise ValueError("库区编码不能重复")
return v

View File

@ -150,7 +150,7 @@ class StorageLocationStatistics(BaseModel):
# 库位状态管理相关模型
class StorageLocationStatusUpdateRequest(BaseModel):
"""库位状态更新请求"""
storage_location_id: str = Field(..., description="库位ID层ID")
layer_name: str = Field(..., description="库位名称")
action: StorageLocationActionEnum = Field(..., description="操作类型")
locked_by: Optional[str] = Field(None, description="锁定者(锁定操作时必填)")
reason: Optional[str] = Field(None, description="操作原因")
@ -158,7 +158,7 @@ class StorageLocationStatusUpdateRequest(BaseModel):
class StorageLocationStatusUpdateResponse(BaseModel):
"""库位状态更新响应"""
storage_location_id: str = Field(..., description="库位ID")
layer_name: str = Field(..., description="库位名称")
action: StorageLocationActionEnum = Field(..., description="执行的操作")
success: bool = Field(..., description="操作是否成功")
message: str = Field(..., description="操作结果消息")
@ -167,7 +167,7 @@ class StorageLocationStatusUpdateResponse(BaseModel):
class BatchStorageLocationStatusUpdateRequest(BaseModel):
"""批量库位状态更新请求"""
storage_location_ids: List[str] = Field(..., description="库位ID列表")
layer_names: List[str] = Field(..., description="库位名称列表")
action: StorageLocationActionEnum = Field(..., description="操作类型")
locked_by: Optional[str] = Field(None, description="锁定者(锁定操作时必填)")
reason: Optional[str] = Field(None, description="操作原因")
@ -280,7 +280,7 @@ class StorageLocationEditRequest(BaseModel):
class StorageLocationEditResponse(BaseModel):
"""库位编辑响应"""
storage_location_id: str = Field(..., description="库位ID")
layer_name: str = Field(..., description="库位名称")
success: bool = Field(..., description="编辑是否成功")
message: str = Field(..., description="操作结果消息")
updated_fields: List[str] = Field(..., description="已更新的字段列表")

View File

@ -2,8 +2,8 @@
# -*- coding: utf-8 -*-
"""
动作点管理API路由
实现动作点和库位的管理功能
库位管理API路由
实现库位的管理功能
"""
from fastapi import APIRouter, Depends, HTTPException, Query
@ -252,11 +252,11 @@ async def batch_update_storage_location_status(
if request.action == StorageLocationActionEnum.LOCK and not request.locked_by:
return error_response("锁定操作必须提供锁定者", 400)
# 验证库位ID列表
if not request.storage_location_ids:
return error_response("库位ID列表不能为空", 400)
# 验证库位名称列表
if not request.layer_names:
return error_response("库位名称列表不能为空", 400)
if len(request.storage_location_ids) > 100:
if len(request.layer_names) > 100:
return error_response("批量操作的库位数量不能超过100个", 400)
# 调用服务层方法
@ -322,16 +322,16 @@ async def get_supported_actions():
return error_response(f"获取支持的操作类型失败: {str(e)}", 500)
@router.get("/{storage_location_id}/status")
@router.get("/{layer_name}/status")
async def get_storage_location_status(
storage_location_id: str,
layer_name: str,
db: Session = Depends(get_db)
):
"""
获取单个库位状态信息
Args:
storage_location_id: 库位ID
layer_name: 库位名称
db: 数据库会话
Returns:
@ -340,7 +340,7 @@ async def get_storage_location_status(
try:
# 查询库位
storage_location = db.query(OperatePointLayer).filter(
OperatePointLayer.id == storage_location_id,
OperatePointLayer.layer_name == layer_name,
OperatePointLayer.is_deleted == False
).first()
@ -505,55 +505,55 @@ async def delete_extended_property(
return error_response(f"删除扩展属性失败: {str(e)}", 500)
@router.get("/extended-properties/types")
async def get_extended_property_types():
"""
获取支持的扩展属性类型列表
# @router.get("/extended-properties/types")
# async def get_extended_property_types():
# """
# 获取支持的扩展属性类型列表
返回系统支持的所有扩展属性类型及其说明
# 返回系统支持的所有扩展属性类型及其说明。
Returns:
ApiResponse: 支持的属性类型列表
"""
try:
from data.models.extended_property import ExtendedPropertyTypeEnum
# 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: "多选 - 适用于多选择"
}
# # 属性类型说明
# 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, "")
})
# 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)
}
)
# 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)
# 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(
storage_location_id: Optional[str] = Query(None, description="库位ID"),
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)"),
@ -571,7 +571,7 @@ async def get_storage_location_operation_logs(
- 其他库位相关的操作
支持多种筛选条件
- 库位ID查询特定库位的操作记录
- 库位名称查询特定库位的操作记录
- 操作人支持模糊搜索操作人姓名
- 操作类型筛选特定类型的操作
- 时间范围指定操作时间的开始和结束时间
@ -579,7 +579,7 @@ async def get_storage_location_operation_logs(
操作记录按时间倒序排列最新的操作在前
Args:
storage_location_id: 库位ID
layer_name: 库位名称
operator: 操作人
operation_type: 操作类型
start_time: 开始时间
@ -616,7 +616,7 @@ async def get_storage_location_operation_logs(
# 构建请求对象
request = StorageLocationLogListRequest(
storage_location_id=storage_location_id,
layer_name=layer_name,
operator=operator,
operation_type=operation_type,
start_time=start_time_dt,
@ -638,9 +638,9 @@ async def get_storage_location_operation_logs(
return error_response(f"获取库位操作记录失败: {str(e)}", 500)
@router.get("/{storage_location_id}", response_model=ApiResponse[StorageLocationDetailResponse])
@router.get("/{layer_name}", response_model=ApiResponse[StorageLocationDetailResponse])
async def get_storage_location_detail(
storage_location_id: str,
layer_name: str,
db: Session = Depends(get_db)
):
"""
@ -653,7 +653,7 @@ async def get_storage_location_detail(
- 状态变更历史记录
Args:
storage_location_id: 库位ID
layer_name: 库位名称
db: 数据库会话
Returns:
@ -661,7 +661,7 @@ async def get_storage_location_detail(
"""
try:
# 调用服务层方法
result = OperatePointService.get_storage_location_detail(db=db, storage_location_id=storage_location_id)
result = OperatePointService.get_storage_location_detail(db=db, layer_name=layer_name)
return api_response(message="获取库位详情成功", data=result)
@ -673,9 +673,9 @@ async def get_storage_location_detail(
return error_response(f"获取库位详情失败: {str(e)}", 500)
@router.put("/{storage_location_id}", response_model=ApiResponse[StorageLocationEditResponse])
@router.put("/{layer_name}", response_model=ApiResponse[StorageLocationEditResponse])
async def edit_storage_location(
storage_location_id: str,
layer_name: str,
request: StorageLocationEditRequest,
db: Session = Depends(get_db)
):
@ -696,7 +696,7 @@ async def edit_storage_location(
- 如果所有字段都没有发生变化会返回相应提示信息
Args:
storage_location_id: 库位ID
layer_name: 库位名称
request: 库位编辑请求
db: 数据库会话
@ -707,7 +707,7 @@ async def edit_storage_location(
# 调用服务层方法
result = OperatePointService.edit_storage_location(
db=db,
storage_location_id=storage_location_id,
layer_name=layer_name,
request=request
)

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@
"""
import uuid
import datetime
from typing import List, Dict, Any, Optional
from sqlalchemy.orm import Session
from sqlalchemy import and_
@ -28,7 +29,12 @@ class MapDataService:
@staticmethod
def push_map_data(db: Session, request: MapDataPushRequest) -> MapDataPushResponse:
"""
推送地图数据
推送地图数据 - 增量更新模式
该方法采用增量更新的方式不会删除现有数据而是在原有基础上增加或更新数据
- 库区基于area_name判断存在则更新不存在则新增
- 动作点基于station_name判断存在则更新不存在则新增
- 分层基于layer_name判断存在则更新不存在则新增
Args:
db: 数据库会话
@ -39,78 +45,32 @@ class MapDataService:
"""
try:
# 初始化计数器
storage_areas_count = 0
operate_points_count = 0
layers_count = 0
storage_areas_created = 0
storage_areas_updated = 0
operate_points_created = 0
operate_points_updated = 0
layers_created = 0
layers_updated = 0
# 先查询数据库是否有相关场景数据
existing_storage_areas = db.query(StorageArea).filter(
and_(
StorageArea.scene_id == request.scene_id,
StorageArea.is_deleted == False
)
).first()
logger.info(f"开始增量推送地图数据: 场景ID={request.scene_id}")
existing_operate_points = db.query(OperatePoint).filter(
and_(
OperatePoint.scene_id == request.scene_id,
OperatePoint.is_deleted == False
)
).first()
# 记录是否进行了覆盖操作
has_existing_data = existing_storage_areas or existing_operate_points
# 如果数据库有相关场景数据,则删除现有数据
if has_existing_data:
logger.info(f"发现现有场景数据,开始删除: 场景ID={request.scene_id}")
MapDataService._delete_existing_data(db, request.scene_id)
else:
logger.info(f"未发现现有场景数据,直接创建: 场景ID={request.scene_id}")
# 处理库区数据 - 创建新数据
logger.info(f"开始创建库区数据: 数量={len(request.storage_areas)}")
# 处理库区数据 - 增量更新
logger.info(f"开始处理库区数据: 数量={len(request.storage_areas)}")
for area_data in request.storage_areas:
new_area = MapDataService._create_storage_area(area_data, request.scene_id, request.operate_points)
db.add(new_area)
storage_areas_count += 1
# 创建库区ID到类型和名称的映射
storage_area_type_mapping = {area.id: area.area_type for area in request.storage_areas}
storage_area_name_mapping = {area.id: area.area_name for area 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 = []
# 查询数据库中该场景下现有的站点名称和库位名称(未删除的)
existing_station_names = set()
existing_location_names = set()
if not has_existing_data: # 如果没有现有数据需要覆盖,则需要检查数据库中的站点名称和库位名称
existing_points = db.query(OperatePoint.station_name, OperatePoint.storage_location_name).filter(
and_(
OperatePoint.scene_id == request.scene_id,
OperatePoint.is_deleted == False
)
).all()
existing_station_names = {point.station_name for point in existing_points}
existing_location_names = {point.storage_location_name for point in existing_points}
# 检查请求中的动作点是否有重复的站点名称和库位名称(包括与数据库中现有数据的重复)
# 检查请求中的动作点是否有重复的站点名称和库位名称
seen_station_names = set()
seen_location_names = set()
for point_data in request.operate_points:
# 检查是否与数据库中现有数据重复
if point_data.station_name in existing_station_names:
duplicate_stations.append(point_data.station_name)
logger.warning(f"发现与数据库中现有数据重复的站点名称: {point_data.station_name}")
continue
if point_data.storage_location_name in existing_location_names:
duplicate_stations.append(f"{point_data.station_name}(库位名重复)")
logger.warning(f"发现与数据库中现有数据重复的库位名称: {point_data.storage_location_name}")
continue
# 检查是否在请求中重复
if point_data.station_name in seen_station_names:
duplicate_stations.append(point_data.station_name)
@ -126,33 +86,37 @@ class MapDataService:
seen_location_names.add(point_data.storage_location_name)
valid_operate_points.append(point_data)
# 处理动作点数据 - 创建新数据
logger.info(f"开始创建动作点数据: 数量={len(valid_operate_points)}")
# 处理动作点数据 - 增量更新
logger.info(f"开始处理动作点数据: 数量={len(valid_operate_points)}")
for point_data in valid_operate_points:
new_point = MapDataService._create_operate_point(point_data, request.scene_id, storage_area_type_mapping, storage_area_name_mapping)
db.add(new_point)
operate_points_count += 1
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._handle_layers(
db, new_point, point_data.layers, is_update=False
layer_counts = MapDataService._upsert_layers(
db, operate_point, point_data.layers
)
layers_count += layer_counts['created']
layers_created += layer_counts['created']
layers_updated += layer_counts['updated']
# 提交事务
db.commit()
logger.info(f"地图数据推送成功: 场景ID={request.scene_id}, "
f"库区={storage_areas_count}个, 动作点={operate_points_count}个, "
f"分层={layers_count}")
f"库区(新增={storage_areas_created},更新={storage_areas_updated}), "
f"动作点(新增={operate_points_created},更新={operate_points_updated}), "
f"分层(新增={layers_created},更新={layers_updated})")
# 根据是否进行了覆盖操作和是否有重复站点生成不同的消息
result_message = ""
if has_existing_data:
result_message = f"推送成功,已覆盖现有数据。创建了{storage_areas_count}个库区,{operate_points_count}个动作点,{layers_count}个分层"
else:
result_message = f"推送成功,创建了{storage_areas_count}个库区,{operate_points_count}个动作点,{layers_count}个分层"
# 生成响应消息
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:
@ -162,9 +126,9 @@ class MapDataService:
return MapDataPushResponse(
scene_id=request.scene_id,
storage_areas_count=storage_areas_count,
operate_points_count=operate_points_count,
layers_count=layers_count,
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
)
@ -173,6 +137,215 @@ class MapDataService:
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_location_name = point_data.storage_location_name
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,
storage_location_name=point_data.storage_location_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,
storage_location_name=operate_point.storage_location_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:
"""
@ -284,33 +457,37 @@ class MapDataService:
@staticmethod
def _delete_existing_data(db: Session, scene_id: str):
"""删除现有数据"""
# 软删除动作点分层
db.query(OperatePointLayer).filter(
OperatePointLayer.operate_point_id.in_(
db.query(OperatePoint.id).filter(
and_(
OperatePoint.scene_id == scene_id,
OperatePoint.is_deleted == False
)
)
# 先获取需要删除的动作点ID列表
operate_point_ids = db.query(OperatePoint.id).filter(
and_(
OperatePoint.scene_id == scene_id,
OperatePoint.is_deleted == False
)
).update({OperatePointLayer.is_deleted: True})
).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
)
).update({OperatePoint.is_deleted: True})
).delete(synchronize_session=False)
# 软删除库区
# 物理删除库区(为了避免主键冲突)
db.query(StorageArea).filter(
and_(
StorageArea.scene_id == scene_id,
StorageArea.is_deleted == False
)
).update({StorageArea.is_deleted: True})
).delete(synchronize_session=False)
@staticmethod
def _calculate_storage_area_capacity(area_type: str, operate_points_data: List) -> int:
@ -349,114 +526,7 @@ class MapDataService:
return total_capacity
@staticmethod
def _create_storage_area(area_data: StorageAreaData, scene_id: str,
operate_points_data: List[OperatePointData]) -> StorageArea:
"""创建库区"""
# 筛选属于该库区的动作点
area_points = [point for point in operate_points_data if point.storage_area_id == area_data.id]
# 系统自动计算容量
max_capacity = MapDataService._calculate_storage_area_capacity(
area_data.area_type.value, area_points
)
return StorageArea(
id=area_data.id,
area_name=area_data.area_name,
area_code=area_data.area_code,
area_type=StorageAreaType(area_data.area_type),
scene_id=scene_id,
max_capacity=max_capacity,
description=area_data.description,
tags=area_data.tags
)
@staticmethod
def _create_operate_point(point_data: OperatePointData, scene_id: str,
storage_area_type_mapping: Dict[str, StorageAreaTypeEnum],
storage_area_name_mapping: Dict[str, str]) -> OperatePoint:
"""创建动作点"""
# 根据库区ID获取库区类型和名称
storage_area_type = None
area_name = None
if point_data.storage_area_id and point_data.storage_area_id in storage_area_type_mapping:
storage_area_type = StorageAreaType(storage_area_type_mapping[point_data.storage_area_id])
area_name = storage_area_name_mapping.get(point_data.storage_area_id)
# 自动生成UUID作为动作点ID
operate_point_id = str(uuid.uuid4())
return OperatePoint(
id=operate_point_id,
station_name=point_data.station_name,
storage_location_name=point_data.storage_location_name,
scene_id=scene_id,
storage_area_id=point_data.storage_area_id,
storage_area_type=storage_area_type,
area_name=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
)
@staticmethod
def _handle_layers(db: Session, operate_point: OperatePoint,
layers_data: Optional[List[OperatePointLayerData]],
is_update: bool = False) -> Dict[str, int]:
"""
处理分层数据
Args:
db: 数据库会话
operate_point: 动作点对象
layers_data: 分层数据列表
is_update: 是否为更新操作现在始终为False
Returns:
Dict[str, int]: 创建的分层数量统计
"""
created_count = 0
if not layers_data:
return {'created': created_count, 'updated': 0}
# 创建新分层
for layer_data in layers_data:
layer_id = str(uuid.uuid4())
new_layer = OperatePointLayer(
id=layer_id,
operate_point_id=operate_point.id,
station_name=operate_point.station_name,
storage_location_name=operate_point.storage_location_name,
area_id=operate_point.storage_area_id, # 添加库区ID冗余字段
area_name=operate_point.area_name, # 添加库区名称
scene_id=operate_point.scene_id, # 添加场景ID冗余字段
layer_index=layer_data.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)}")
# 不抛出异常,避免影响地图推送的主流程
created_count += 1
return {'created': created_count, 'updated': 0}
@staticmethod
def _sync_extended_properties_to_new_layer(db: Session, layer: OperatePointLayer):

View File

@ -199,15 +199,15 @@ class OperatePointService:
StorageLocationStatusUpdateResponse: 更新响应
"""
try:
# 查询库位
# 查询库位使用layer_name进行查询
storage_location = db.query(OperatePointLayer).filter(
OperatePointLayer.id == request.storage_location_id,
OperatePointLayer.layer_name == request.layer_name,
OperatePointLayer.is_deleted == False
).first()
if not storage_location:
return StorageLocationStatusUpdateResponse(
storage_location_id=request.storage_location_id,
layer_name=request.layer_name,
action=request.action,
success=False,
message="库位不存在",
@ -227,7 +227,7 @@ class OperatePointService:
db=db,
operator=operator,
operation_type=request.action.value,
affected_storage_locations=[request.storage_location_id],
affected_storage_locations=[request.layer_name],
description=request.reason
)
except Exception as e:
@ -237,7 +237,7 @@ class OperatePointService:
new_status = OperatePointService._get_storage_location_status(storage_location)
return StorageLocationStatusUpdateResponse(
storage_location_id=request.storage_location_id,
layer_name=request.layer_name,
action=request.action,
success=True,
message=message,
@ -246,7 +246,7 @@ class OperatePointService:
else:
db.rollback()
return StorageLocationStatusUpdateResponse(
storage_location_id=request.storage_location_id,
layer_name=request.layer_name,
action=request.action,
success=False,
message=message,
@ -257,7 +257,7 @@ class OperatePointService:
db.rollback()
logger.error(f"更新库位状态失败: {str(e)}")
return StorageLocationStatusUpdateResponse(
storage_location_id=request.storage_location_id,
layer_name=request.layer_name,
action=request.action,
success=False,
message=f"更新库位状态失败: {str(e)}",
@ -281,10 +281,10 @@ class OperatePointService:
success_count = 0
failed_count = 0
for storage_location_id in request.storage_location_ids:
for layer_name in request.layer_names:
# 创建单个更新请求
single_request = StorageLocationStatusUpdateRequest(
storage_location_id=storage_location_id,
layer_name=layer_name,
action=request.action,
locked_by=request.locked_by,
reason=request.reason
@ -300,7 +300,7 @@ class OperatePointService:
failed_count += 1
return BatchStorageLocationStatusUpdateResponse(
total_count=len(request.storage_location_ids),
total_count=len(request.layer_names),
success_count=success_count,
failed_count=failed_count,
results=results
@ -404,7 +404,10 @@ class OperatePointService:
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:
@ -414,6 +417,13 @@ class OperatePointService:
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:
@ -1026,13 +1036,13 @@ class OperatePointService:
raise
@staticmethod
def get_storage_location_detail(db: Session, storage_location_id: str) -> Dict[str, Any]:
def get_storage_location_detail(db: Session, layer_name: str) -> Dict[str, Any]:
"""
获取库位详情
Args:
db: 数据库会话
storage_location_id: 库位ID
layer_name: 库位名称
Returns:
Dict[str, Any]: 库位详情信息
@ -1040,12 +1050,12 @@ class OperatePointService:
try:
# 查询库位
layer = db.query(OperatePointLayer).filter(
OperatePointLayer.id == storage_location_id,
OperatePointLayer.layer_name == layer_name,
OperatePointLayer.is_deleted == False
).first()
if not layer:
raise ValueError(f"库位 {storage_location_id} 不存在")
raise ValueError(f"库位 {layer_name} 不存在")
# 获取库位详细信息
storage_location_info = OperatePointService._convert_to_storage_location_info(
@ -1109,13 +1119,13 @@ class OperatePointService:
raise
@staticmethod
def edit_storage_location(db: Session, storage_location_id: str, request: Any) -> Dict[str, Any]:
def edit_storage_location(db: Session, layer_name: str, request: Any) -> Dict[str, Any]:
"""
编辑库位信息
Args:
db: 数据库会话
storage_location_id: 库位ID
layer_name: 库位名称
request: 库位编辑请求
Returns:
@ -1124,12 +1134,12 @@ class OperatePointService:
try:
# 查询库位
layer = db.query(OperatePointLayer).filter(
OperatePointLayer.id == storage_location_id,
OperatePointLayer.layer_name == layer_name,
OperatePointLayer.is_deleted == False
).first()
if not layer:
raise ValueError(f"库位 {storage_location_id} 不存在")
raise ValueError(f"库位 {layer_name} 不存在")
# 跟踪更新的字段
updated_fields = []
@ -1236,7 +1246,7 @@ class OperatePointService:
# 检查是否有实际的字段变更
if not updated_fields:
return {
"storage_location_id": storage_location_id,
"layer_name": layer_name,
"success": True,
"message": "没有字段发生变化,数据保持不变",
"updated_fields": [],
@ -1259,7 +1269,7 @@ class OperatePointService:
db=db,
operator=operator,
operation_type="编辑库位",
affected_storage_locations=[storage_location_id],
affected_storage_locations=[layer_name],
description=f"编辑库位信息,更新字段: {', '.join(updated_fields)}"
)
except Exception as e:
@ -1271,7 +1281,7 @@ class OperatePointService:
)
return {
"storage_location_id": storage_location_id,
"layer_name": layer_name,
"success": True,
"message": f"库位信息更新成功,共更新 {len(updated_fields)} 个字段",
"updated_fields": updated_fields,

View File

@ -1,410 +0,0 @@
# 扩展属性管理接口文档
## 概述
扩展属性管理接口用于管理动作点(库位)的扩展属性定义。通过这些接口,您可以创建、查询和删除扩展属性配置,为库位提供灵活的自定义字段支持。
## 接口列表
### 1. 创建扩展属性
**接口地址:** `POST /api/vwed-operate-point/extended-properties`
**功能说明:** 创建新的扩展属性定义
**重要提示:** 创建扩展属性后会自动将该属性添加到所有现有的库位层中每个库位层的config_json会自动更新包含新的扩展属性配置。
**请求参数:**
```json
{
"property_name": "温度",
"property_type": "float",
"is_required": false,
"is_enabled": true,
"description": "库位温度监控",
"placeholder": "请输入温度值",
"default_value": "25.0",
"options": null,
"validation_rules": {
"min": -50,
"max": 100
},
"category": "环境监控",
"sort_order": 10,
"display_width": 150,
"display_format": "{{value}}°C"
}
```
**响应示例:**
```json
{
"code": 200,
"message": "扩展属性创建成功",
"data": {
"id": "1",
"property_name": "温度",
"message": "扩展属性创建成功,已应用到所有库位层"
}
}
```
### 2. 获取扩展属性列表
**接口地址:** `GET /api/vwed-operate-point/extended-properties`
**功能说明:** 获取扩展属性列表,支持多种筛选条件
**请求参数:**
- `property_name` (可选): 属性名称,支持模糊搜索
- `property_type` (可选): 属性类型
- `category` (可选): 属性分类
- `is_enabled` (可选): 是否启用
- `page` (可选): 页码默认1
- `page_size` (可选): 每页数量默认20
**响应示例:**
```json
{
"code": 200,
"message": "查询成功",
"data": {
"total": 5,
"page": 1,
"page_size": 20,
"total_pages": 1,
"properties": [
{
"id": "1",
"property_name": "温度",
"property_type": "float",
"is_required": false,
"is_enabled": true,
"description": "库位温度监控",
"placeholder": "请输入温度值",
"default_value": "25.0",
"options": null,
"validation_rules": {
"min": -50,
"max": 100
},
"category": "环境监控",
"sort_order": 10,
"display_width": 150,
"display_format": "{{value}}°C",
"created_at": "2024-12-20T10:00:00",
"updated_at": "2024-12-20T10:00:00"
}
]
}
}
```
### 3. 删除扩展属性
**接口地址:** `DELETE /api/vwed-operate-point/extended-properties/{property_id}`
**功能说明:** 删除指定的扩展属性(软删除)
**重要提示:** 删除扩展属性后会自动从所有现有的库位层中移除该属性每个库位层的config_json会自动更新清除已删除的扩展属性配置。此操作不可逆请谨慎使用。
**请求参数:**
- `property_id` (路径参数): 属性ID
**响应示例:**
```json
{
"code": 200,
"message": "扩展属性删除成功",
"data": {
"property_id": "1",
"property_name": "温度",
"message": "扩展属性删除成功,已从所有库位层移除"
}
}
```
### 4. 获取扩展属性类型列表
**接口地址:** `GET /api/vwed-operate-point/extended-properties/types`
**功能说明:** 获取系统支持的扩展属性类型列表
**响应示例:**
```json
{
"code": 200,
"message": "获取扩展属性类型成功",
"data": {
"types": [
{
"value": "string",
"description": "字符串 - 适用于短文本输入"
},
{
"value": "integer",
"description": "整数 - 适用于整数值"
},
{
"value": "float",
"description": "浮点数 - 适用于小数值"
},
{
"value": "boolean",
"description": "布尔值 - 适用于是/否选择"
},
{
"value": "date",
"description": "日期 - 适用于日期选择"
},
{
"value": "datetime",
"description": "日期时间 - 适用于日期和时间选择"
},
{
"value": "text",
"description": "长文本 - 适用于多行文本输入"
},
{
"value": "select",
"description": "下拉选择 - 适用于单选择"
},
{
"value": "multiselect",
"description": "多选 - 适用于多选择"
}
],
"count": 9
}
}
```
## 属性类型详细说明
### 1. string字符串
- 适用于短文本输入
- 支持长度限制验证
- 支持正则表达式验证
### 2. integer整数
- 适用于整数值
- 支持最小值/最大值验证
- 支持数值范围验证
### 3. float浮点数
- 适用于小数值
- 支持最小值/最大值验证
- 支持精度设置
### 4. boolean布尔值
- 适用于是/否选择
- 显示为复选框或开关
### 5. date日期
- 适用于日期选择
- 格式YYYY-MM-DD
- 支持日期范围验证
### 6. datetime日期时间
- 适用于日期和时间选择
- 格式YYYY-MM-DD HH:MM:SS
- 支持日期时间范围验证
### 7. text长文本
- 适用于多行文本输入
- 支持最大长度限制
- 显示为文本区域
### 8. select下拉选择
- 适用于单选择
- 需要配置选项列表
- 选项格式:`[{"label": "选项1", "value": "value1"}, ...]`
### 9. multiselect多选
- 适用于多选择
- 需要配置选项列表
- 选项格式:`[{"label": "选项1", "value": "value1"}, ...]`
## 验证规则配置
验证规则使用JSON格式配置支持以下规则
```json
{
"min": 0, // 最小值(数字类型)
"max": 100, // 最大值(数字类型)
"minLength": 1, // 最小长度(字符串类型)
"maxLength": 255, // 最大长度(字符串类型)
"pattern": "^[a-zA-Z0-9]+$", // 正则表达式(字符串类型)
"required": true, // 是否必填
"decimal": 2 // 小数位数(浮点数类型)
}
```
## 选项配置示例
对于select和multiselect类型选项配置示例
```json
[
{
"label": "选项1",
"value": "option1",
"description": "选项1的描述"
},
{
"label": "选项2",
"value": "option2",
"description": "选项2的描述"
}
]
```
## 使用示例
### 创建温度监控属性
```bash
curl -X POST "http://localhost:8000/api/vwed-operate-point/extended-properties" \
-H "Content-Type: application/json" \
-d '{
"property_name": "温度",
"property_type": "float",
"description": "库位温度监控",
"validation_rules": {
"min": -50,
"max": 100,
"decimal": 1
},
"category": "环境监控",
"display_format": "{{value}}°C"
}'
```
### 创建库位状态选择属性
```bash
curl -X POST "http://localhost:8000/api/vwed-operate-point/extended-properties" \
-H "Content-Type: application/json" \
-d '{
"property_name": "维护状态",
"property_type": "select",
"is_required": true,
"options": [
{"label": "正常", "value": "normal"},
{"label": "维护中", "value": "maintenance"},
{"label": "故障", "value": "fault"}
],
"category": "设备管理"
}'
```
## 扩展属性在库位层中的存储
### config_json结构
每个库位层的扩展属性值存储在 `config_json` 字段中,结构如下:
```json
{
"extended_fields": {
"温度": {
"value": "25.0",
"type": "float",
"is_required": false,
"updated_at": "2024-12-20T10:00:00"
},
"维护状态": {
"value": "normal",
"type": "select",
"is_required": true,
"updated_at": "2024-12-20T10:00:00"
}
}
}
```
### 自动同步机制
1. **创建扩展属性时:** 自动为所有现有库位层添加该属性,使用默认值进行初始化
2. **删除扩展属性时:** 自动从所有库位层中移除该属性的配置和数据
3. **新建库位层时:** 自动同步所有已启用的扩展属性到新库位层
4. **地图推送时:** 推送地图数据创建新库位层时,自动为每个新层同步扩展属性
### 数据完整性保证
- 扩展属性定义与库位层数据保持同步
- 删除扩展属性会清理所有相关数据
- 支持批量操作,确保数据一致性
## 与地图推送的集成
### 地图推送时的自动同步
当通过地图推送接口 `POST /api/vwed-map-data/push` 创建新的地图数据时:
1. **自动检测扩展属性:** 系统会查询所有已启用的扩展属性
2. **同步到新库位层:** 为每个新创建的库位层自动添加扩展属性配置
3. **使用默认值:** 新库位层的扩展属性会使用定义时的默认值进行初始化
4. **错误处理:** 即使扩展属性同步失败,地图推送也能正常完成
### 推荐工作流程
1. **先配置扩展属性:** 在推送地图前,先创建所需的扩展属性定义
2. **推送地图数据:** 地图推送时会自动为新库位层同步扩展属性
3. **验证结果:** 通过库位列表接口查看扩展属性是否正确同步
### 示例场景
```bash
# 1. 创建温度监控属性
curl -X POST "http://localhost:8000/api/vwed-operate-point/extended-properties" \
-H "Content-Type: application/json" \
-d '{
"property_name": "温度",
"property_type": "float",
"default_value": "25.0"
}'
# 2. 推送地图数据
curl -X POST "http://localhost:8000/api/vwed-map-data/push" \
-H "Content-Type: application/json" \
-d '{
"scene_id": "scene_001",
"storage_areas": [...],
"operate_points": [...]
}'
# 3. 查看库位列表(新库位层会自动包含温度属性)
curl "http://localhost:8000/api/vwed-operate-point/list?include_extended_fields=true"
```
## 注意事项
1. **属性名称唯一性:** `property_name` 必须在系统中唯一
2. **软删除:** 删除操作是软删除,不会真正删除数据
3. **数据类型验证:** 创建时会验证数据类型和格式
4. **选项配置:** select和multiselect类型需要配置选项列表
5. **验证规则:** 验证规则必须符合JSON格式且类型匹配
6. **自动同步:** 扩展属性的创建和删除会自动影响所有库位层的配置
7. **地图推送集成:** 新推送的地图数据会自动包含已定义的扩展属性
8. **操作顺序:** 建议先创建扩展属性,再进行地图推送,以确保新库位层包含完整的属性配置
9. **性能考虑:** 扩展属性数量较多时,地图推送可能需要更多时间来同步属性
## 错误码说明
- `200`: 操作成功
- `400`: 请求参数错误
- `404`: 资源不存在
- `500`: 服务器内部错误
## 数据库迁移
在使用这些接口前,请确保已执行数据库迁移:
```bash
# 执行迁移
python scripts/run_migration.py
```
迁移将创建 `extended_properties` 表及相关索引。