544 lines
22 KiB
Python
544 lines
22 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
VWED库位相关内置模块
|
||
提供在线脚本中库位操作相关功能
|
||
"""
|
||
|
||
import json
|
||
from typing import Any, Dict
|
||
from sqlalchemy import and_, select
|
||
|
||
from utils.logger import get_logger
|
||
from utils.json_parser import safe_parse_dict
|
||
from data.session import get_async_session
|
||
from data.models.operate_point import OperatePoint
|
||
from data.models.operate_point_layer import OperatePointLayer
|
||
from data.models.storage_area import StorageArea
|
||
|
||
logger = get_logger("services.online_script.location_module")
|
||
|
||
|
||
class VWEDLocationModule:
|
||
"""VWED库位模块"""
|
||
|
||
def __init__(self, script_id: str):
|
||
self.script_id = script_id
|
||
logger.debug(f"初始化库位模块,脚本ID: {script_id}")
|
||
|
||
async def is_point_exist(self, point_name: str) -> bool:
|
||
"""
|
||
检查站点是否存在
|
||
|
||
Args:
|
||
point_name: 站点名
|
||
|
||
Returns:
|
||
bool: True表示站点存在,False表示不存在
|
||
"""
|
||
try:
|
||
async with get_async_session() as db:
|
||
result = await db.execute(
|
||
select(OperatePoint).filter(
|
||
OperatePoint.station_name == point_name,
|
||
OperatePoint.is_deleted == False
|
||
)
|
||
)
|
||
point = result.scalars().first()
|
||
return point is not None
|
||
|
||
except Exception as e:
|
||
logger.error(f"检查站点是否存在失败: {str(e)}")
|
||
raise
|
||
|
||
async def check_site_existed_by_site_id(self, site_id: str) -> bool:
|
||
"""
|
||
检查库位是否存在
|
||
|
||
Args:
|
||
site_id: 库位名称
|
||
|
||
Returns:
|
||
bool: True表示库位存在,False表示不存在
|
||
"""
|
||
try:
|
||
async with get_async_session() as db:
|
||
result = await db.execute(
|
||
select(OperatePointLayer).filter(
|
||
OperatePointLayer.layer_name == site_id,
|
||
OperatePointLayer.is_deleted == False
|
||
)
|
||
)
|
||
layer = result.scalars().first()
|
||
return layer is not None
|
||
|
||
except Exception as e:
|
||
logger.error(f"检查库位是否存在失败: {str(e)}")
|
||
raise
|
||
|
||
async def check_site_group_existed_by_group_name(self, group_name: str) -> bool:
|
||
"""
|
||
检查库区是否存在
|
||
|
||
Args:
|
||
group_name: 库区名称
|
||
|
||
Returns:
|
||
bool: True表示库区存在,False表示不存在
|
||
"""
|
||
try:
|
||
async with get_async_session() as db:
|
||
result = await db.execute(
|
||
select(StorageArea).filter(
|
||
StorageArea.area_name == group_name,
|
||
StorageArea.is_deleted == False
|
||
)
|
||
)
|
||
area = result.scalars().first()
|
||
return area is not None
|
||
|
||
except Exception as e:
|
||
logger.error(f"检查库区是否存在失败: {str(e)}")
|
||
raise
|
||
|
||
def _build_field_conditions(self, field_attr, field_values):
|
||
"""构建单个字段的查询条件,支持列表AND逻辑和模糊匹配"""
|
||
if not isinstance(field_values, list):
|
||
field_values = [field_values]
|
||
|
||
and_conditions = []
|
||
for value in field_values:
|
||
if isinstance(value, str) and value.startswith("'") and value.endswith("'") and "%" in value:
|
||
# 模糊查询:'%pattern%' 格式
|
||
pattern = value.strip("'")
|
||
and_conditions.append(field_attr.like(pattern))
|
||
else:
|
||
# 精确匹配
|
||
and_conditions.append(field_attr == value)
|
||
|
||
# 使用AND逻辑连接所有条件
|
||
if len(and_conditions) == 1:
|
||
return and_conditions[0]
|
||
else:
|
||
return and_(*and_conditions)
|
||
|
||
def _build_query_filters(self, conditions: Dict[str, Any]):
|
||
"""构建查询过滤条件"""
|
||
query = select(OperatePointLayer).filter(OperatePointLayer.is_deleted == False)
|
||
|
||
# 字段映射配置
|
||
field_mappings = {
|
||
"site_ids": OperatePointLayer.layer_name,
|
||
"area": OperatePointLayer.area_name,
|
||
"group_names": OperatePointLayer.area_name,
|
||
"content": OperatePointLayer.goods_content,
|
||
"tags": OperatePointLayer.tags,
|
||
"no": OperatePointLayer.layer_name,
|
||
"type": OperatePointLayer.location_type,
|
||
"locked_by": OperatePointLayer.locked_by,
|
||
}
|
||
|
||
# 处理字段映射的条件
|
||
for field_name, field_attr in field_mappings.items():
|
||
if field_name in conditions:
|
||
condition = self._build_field_conditions(field_attr, conditions[field_name])
|
||
query = query.filter(condition)
|
||
|
||
# 处理布尔字段
|
||
if "filled" in conditions:
|
||
filled = conditions["filled"]
|
||
if isinstance(filled, bool):
|
||
query = query.filter(OperatePointLayer.is_occupied == filled)
|
||
elif isinstance(filled, list):
|
||
# 如果是列表,使用AND逻辑(虽然布尔字段用列表不太常见)
|
||
bool_conditions = []
|
||
for val in filled:
|
||
if isinstance(val, bool):
|
||
bool_conditions.append(OperatePointLayer.is_occupied == val)
|
||
if bool_conditions:
|
||
query = query.filter(and_(*bool_conditions))
|
||
|
||
if "locked" in conditions:
|
||
locked = conditions["locked"]
|
||
if isinstance(locked, bool):
|
||
query = query.filter(OperatePointLayer.is_locked == locked)
|
||
elif isinstance(locked, list):
|
||
bool_conditions = []
|
||
for val in locked:
|
||
if isinstance(val, bool):
|
||
bool_conditions.append(OperatePointLayer.is_locked == val)
|
||
if bool_conditions:
|
||
query = query.filter(and_(*bool_conditions))
|
||
|
||
return query
|
||
|
||
async def find_sites_by_condition(self, conditions: str, sort: str) -> str:
|
||
"""
|
||
根据条件获取库位
|
||
|
||
Args:
|
||
conditions: 查询条件JSON字符串
|
||
sort: 排序方式,"ASC"升序,"DESC"降序
|
||
|
||
Returns:
|
||
str: 库位列表JSON字符串,如果没有找到则返回"null"
|
||
"""
|
||
try:
|
||
condition_dict = safe_parse_dict(conditions, self.script_id)
|
||
if condition_dict is None:
|
||
return "null"
|
||
|
||
async with get_async_session() as db:
|
||
query = self._build_query_filters(condition_dict)
|
||
# 排序
|
||
if sort.upper() == "DESC":
|
||
query = query.order_by(OperatePointLayer.layer_name.desc())
|
||
else:
|
||
query = query.order_by(OperatePointLayer.layer_name.asc())
|
||
|
||
result = await db.execute(query)
|
||
results = result.scalars().all()
|
||
|
||
if not results:
|
||
return "null"
|
||
|
||
# 转换为返回格式
|
||
site_list = []
|
||
for site in results:
|
||
site_data = {
|
||
"area": site.area_name or "",
|
||
"content": site.goods_content or "",
|
||
"depth": "", # 需要根据实际需求映射
|
||
"disabled": site.is_disabled,
|
||
"filled": 1 if site.is_occupied else 0,
|
||
"group_names": site.area_name or "",
|
||
"level": "", # 需要根据实际需求映射
|
||
"locked": 1 if site.is_locked else 0,
|
||
"locked_by": site.locked_by or "",
|
||
"no": site.layer_name or "",
|
||
"preparing": False, # 需要根据实际业务逻辑确定
|
||
"row_num": "", # 需要根据实际需求映射
|
||
"site_id": site.layer_name or "",
|
||
"tags": site.tags or "",
|
||
"type": site.location_type,
|
||
"working": False # 需要根据实际业务逻辑确定
|
||
}
|
||
site_list.append(site_data)
|
||
|
||
return json.dumps(site_list, ensure_ascii=False)
|
||
|
||
except Exception as e:
|
||
logger.error(f"根据条件获取库位失败: {str(e)}")
|
||
raise
|
||
|
||
async def find_available_sites_by_condition(self, conditions: str, sort: str) -> str:
|
||
"""
|
||
根据条件获取有效库位(未禁用且同步成功的库位)
|
||
|
||
Args:
|
||
conditions: 查询条件JSON字符串
|
||
sort: 排序方式,"ASC"升序,"DESC"降序
|
||
|
||
Returns:
|
||
str: 库位列表JSON字符串,如果没有找到则返回"null"
|
||
"""
|
||
try:
|
||
condition_dict = safe_parse_dict(conditions, self.script_id)
|
||
if condition_dict is None:
|
||
return "null"
|
||
|
||
async with get_async_session() as db:
|
||
query = self._build_query_filters(condition_dict)
|
||
|
||
# 添加有效库位的过滤条件:未禁用且同步成功
|
||
query = query.filter(
|
||
OperatePointLayer.is_disabled == False
|
||
# 这里假设sync_failed字段存在,如果不存在可以去掉这个条件
|
||
# OperatePointLayer.sync_failed == False
|
||
)
|
||
|
||
# 排序
|
||
if sort.upper() == "DESC":
|
||
query = query.order_by(OperatePointLayer.layer_name.desc())
|
||
else:
|
||
query = query.order_by(OperatePointLayer.layer_name.asc())
|
||
|
||
result = await db.execute(query)
|
||
results = result.scalars().all()
|
||
|
||
if not results:
|
||
return "null"
|
||
|
||
# 转换为返回格式
|
||
site_list = []
|
||
for site in results:
|
||
site_data = {
|
||
"area": site.area_name or "",
|
||
"content": site.goods_content or "",
|
||
"depth": "",
|
||
"disabled": site.is_disabled,
|
||
"filled": 1 if site.is_occupied else 0,
|
||
"group_names": site.area_name or "",
|
||
"level": "",
|
||
"locked": 1 if site.is_locked else 0,
|
||
"locked_by": site.locked_by or "",
|
||
"no": site.layer_name or "",
|
||
"preparing": False,
|
||
"row_num": "",
|
||
"site_id": site.layer_name or "",
|
||
"tags": site.tags or "",
|
||
"type": site.location_type,
|
||
"working": False
|
||
}
|
||
site_list.append(site_data)
|
||
|
||
return json.dumps(site_list, ensure_ascii=False)
|
||
|
||
except Exception as e:
|
||
logger.error(f"根据条件获取有效库位失败: {str(e)}")
|
||
raise
|
||
|
||
async def find_available_sites_by_ext_fields(self, conditions: str) -> str:
|
||
"""
|
||
根据扩展字段获取有效库位
|
||
|
||
Args:
|
||
conditions: 查询条件JSON字符串,包含扩展字段条件
|
||
|
||
Returns:
|
||
str: 库位列表JSON字符串,如果没有找到则返回"null"
|
||
"""
|
||
try:
|
||
from utils.json_parser import safe_parse_list
|
||
condition_list = safe_parse_list(conditions, self.script_id)
|
||
if condition_list is None:
|
||
return "null"
|
||
|
||
async with get_async_session() as db:
|
||
query = select(OperatePointLayer).filter(
|
||
OperatePointLayer.is_deleted == False,
|
||
OperatePointLayer.is_disabled == False
|
||
)
|
||
|
||
# 处理扩展字段条件
|
||
from sqlalchemy import text
|
||
for i, condition in enumerate(condition_list):
|
||
attr_name = condition.get("attribute_name")
|
||
attr_value = condition.get("attribute_value")
|
||
|
||
# 处理字段名,确保JSON路径正确
|
||
# 如果字段名是纯数字或包含特殊字符,需要用引号包围
|
||
if attr_name.isdigit() or not attr_name.isidentifier():
|
||
json_path = f'$.extended_fields."{attr_name}".value'
|
||
else:
|
||
json_path = f"$.extended_fields.{attr_name}.value"
|
||
|
||
|
||
if isinstance(attr_value, str) and "%" in attr_value:
|
||
# 模糊查询 - 使用JSON_UNQUOTE去除JSON字符串的引号
|
||
param_name = f"pattern_{i}"
|
||
sql_condition = f"JSON_UNQUOTE(JSON_EXTRACT(config_json, '{json_path}')) LIKE :{param_name}"
|
||
query = query.filter(text(sql_condition).params(**{param_name: attr_value}))
|
||
else:
|
||
# 精确查询
|
||
param_name = f"value_{i}"
|
||
sql_condition = f"JSON_EXTRACT(config_json, '{json_path}') = :{param_name}"
|
||
query = query.filter(text(sql_condition).params(**{param_name: attr_value}))
|
||
|
||
result = await db.execute(query)
|
||
results = result.scalars().all()
|
||
|
||
if not results:
|
||
return "null"
|
||
|
||
# 转换为返回格式
|
||
site_list = []
|
||
for site in results:
|
||
site_data = {
|
||
"area": site.area_name,
|
||
"content": site.goods_content,
|
||
"filled": 1 if site.is_occupied else 0,
|
||
"group_name": site.area_name,
|
||
"locked": 1 if site.is_locked else 0,
|
||
"locked_by": site.locked_by,
|
||
"site_id": site.layer_name,
|
||
"site_name": site.layer_name,
|
||
"tags": site.tags,
|
||
"type": site.location_type
|
||
}
|
||
site_list.append(site_data)
|
||
|
||
return json.dumps(site_list, ensure_ascii=False)
|
||
|
||
except Exception as e:
|
||
logger.error(f"根据扩展字段获取有效库位失败: {str(e)}")
|
||
raise
|
||
|
||
async def update_sites_by_condition(self, conditions: str, values: str) -> int:
|
||
"""
|
||
根据条件更新库位
|
||
|
||
Args:
|
||
conditions: 更新条件JSON字符串
|
||
values: 修改的字段JSON字符串
|
||
|
||
Returns:
|
||
int: 更新成功的数据库记录行数
|
||
"""
|
||
try:
|
||
condition_dict = safe_parse_dict(conditions, self.script_id)
|
||
value_dict = safe_parse_dict(values, self.script_id)
|
||
|
||
if condition_dict is None or value_dict is None:
|
||
return 0
|
||
|
||
async with get_async_session() as db:
|
||
from sqlalchemy import update
|
||
|
||
# 构建更新字段
|
||
update_data = {}
|
||
|
||
# 映射字段名
|
||
field_mapping = {
|
||
"area": "area_name",
|
||
"content": "goods_content",
|
||
"disabled": "is_disabled",
|
||
"filled": "is_occupied",
|
||
"group_name": "area_name",
|
||
"locked": "is_locked",
|
||
"locked_by": "locked_by",
|
||
"site_id": "layer_name",
|
||
"tags": "tags",
|
||
"type": "location_type",
|
||
"preparing": None, # 暂时不支持
|
||
"working": None, # 暂时不支持
|
||
}
|
||
|
||
for key, value in value_dict.items():
|
||
if key in field_mapping and field_mapping[key] is not None:
|
||
update_data[field_mapping[key]] = value
|
||
|
||
if not update_data:
|
||
return 0
|
||
|
||
# 检查是否需要更新库区名称,如果是则需要同步相关表
|
||
area_name_update = update_data.get("area_name")
|
||
if area_name_update:
|
||
# 先获取旧的库区名称,用于同步相关表
|
||
query_old_areas = self._build_query_filters(condition_dict)
|
||
result = await db.execute(query_old_areas)
|
||
affected_sites = result.scalars().all()
|
||
|
||
old_area_names = set()
|
||
for site in affected_sites:
|
||
if site.area_name:
|
||
old_area_names.add(site.area_name)
|
||
|
||
# 构建更新查询
|
||
filtered_query = self._build_query_filters(condition_dict)
|
||
|
||
# 执行OperatePointLayer表的更新
|
||
stmt = update(OperatePointLayer).values(**update_data)
|
||
|
||
# 应用同样的过滤条件
|
||
for condition in filtered_query.whereclause.clauses if hasattr(filtered_query.whereclause, 'clauses') else [filtered_query.whereclause]:
|
||
stmt = stmt.where(condition)
|
||
|
||
result = await db.execute(stmt)
|
||
affected_rows = result.rowcount
|
||
|
||
# 如果修改了库区名称,需要同步相关表
|
||
if area_name_update and old_area_names:
|
||
logger.debug(f"同步库区名称更新: {old_area_names} -> {area_name_update}")
|
||
|
||
# 更新 OperatePoint 表中的 area_name
|
||
from data.models.operate_point import OperatePoint
|
||
for old_area_name in old_area_names:
|
||
await db.execute(
|
||
update(OperatePoint)
|
||
.where(
|
||
and_(
|
||
OperatePoint.area_name == old_area_name,
|
||
OperatePoint.is_deleted == False
|
||
)
|
||
)
|
||
.values(area_name=area_name_update)
|
||
)
|
||
|
||
# 更新 StorageArea 表中的 area_name
|
||
from data.models.storage_area import StorageArea
|
||
for old_area_name in old_area_names:
|
||
await db.execute(
|
||
update(StorageArea)
|
||
.where(
|
||
and_(
|
||
StorageArea.area_name == old_area_name,
|
||
StorageArea.is_deleted == False
|
||
)
|
||
)
|
||
.values(area_name=area_name_update)
|
||
)
|
||
|
||
await db.commit()
|
||
return affected_rows
|
||
|
||
except Exception as e:
|
||
logger.error(f"根据条件更新库位失败: {str(e)}")
|
||
raise
|
||
|
||
async def update_site_ext_field_by_id_and_ext_field_name(self, conditions: str) -> None:
|
||
"""
|
||
根据条件更新库位扩展字段值
|
||
|
||
Args:
|
||
conditions: 库位ID、扩展字段名和更新值的JSON字符串
|
||
"""
|
||
try:
|
||
from utils.json_parser import safe_parse_list
|
||
condition_list = safe_parse_list(conditions, self.script_id)
|
||
if condition_list is None:
|
||
return
|
||
|
||
async with get_async_session() as db:
|
||
for condition in condition_list:
|
||
site_id = condition.get("site_id")
|
||
ext_field_name = condition.get("ext_field_name")
|
||
update_value = condition.get("update_value")
|
||
|
||
if not site_id or not ext_field_name:
|
||
continue
|
||
|
||
# 查询库位
|
||
result = await db.execute(
|
||
select(OperatePointLayer).filter(
|
||
OperatePointLayer.layer_name == site_id,
|
||
OperatePointLayer.is_deleted == False
|
||
)
|
||
)
|
||
site = result.scalars().first()
|
||
|
||
if site:
|
||
# 解析现有的config_json
|
||
config_data = {}
|
||
if site.config_json:
|
||
try:
|
||
config_data = json.loads(site.config_json)
|
||
except:
|
||
config_data = {}
|
||
|
||
# 更新扩展字段
|
||
if "extended_fields" not in config_data:
|
||
config_data["extended_fields"] = {}
|
||
|
||
config_data["extended_fields"][ext_field_name] = update_value
|
||
|
||
# 保存更新后的config_json
|
||
site.config_json = json.dumps(config_data, ensure_ascii=False)
|
||
|
||
except Exception as e:
|
||
logger.error(f"根据条件更新库位扩展字段值失败: {str(e)}")
|
||
raise
|
||
|
||
|
||
__all__ = ['VWEDLocationModule'] |