修复:消除托盘与用户分配查询竞态

This commit is contained in:
焦龙言
2026-07-26 12:36:00 +08:00
parent 6851499be2
commit aba8a96cb8
3 changed files with 446 additions and 111 deletions
@@ -2,12 +2,13 @@ from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel
from sqlalchemy import ColumnElement, Select, delete, func, or_, select, update
from sqlalchemy import ColumnElement, Select, and_, delete, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel
from module_admin.entity.do.user_do import SysUser
from module_business.domain.enums import PalletFlowState
from module_business.entity.do.master_do import (
BizCustomer,
BizCustomerSite,
@@ -20,10 +21,13 @@ from module_business.entity.do.master_do import (
)
from module_business.entity.do.operation_do import (
BizFillingRecord,
BizInboundItem,
BizInboundOrder,
BizPalletEvent,
BizPalletInventory,
BizReturnItem,
BizReturnOrder,
BizShipmentItem,
BizShipmentOrder,
BizUsageRecord,
)
@@ -74,6 +78,15 @@ _REFERENCE_COLUMNS: dict[MasterResource, tuple[tuple[InstrumentedAttribute, ...]
),
}
_PALLET_REMOVAL_BLOCKING_COLUMNS: tuple[InstrumentedAttribute, ...] = (
BizInboundItem.pallet_id,
BizFillingRecord.pallet_id,
BizShipmentItem.pallet_id,
BizUsageRecord.pallet_id,
BizReturnItem.pallet_id,
BizPalletEvent.pallet_id,
)
class MasterDao:
"""菌种、客户、现场、产线和仓库主数据数据库操作层。"""
@@ -99,8 +112,13 @@ class MasterDao:
model: type[Any],
id_column: InstrumentedAttribute,
resource_id: int,
*,
for_update: bool = False,
) -> Any | None:
result = await db.execute(select(model).where(id_column == resource_id))
statement = select(model).where(id_column == resource_id)
if for_update:
statement = statement.with_for_update()
result = await db.execute(statement)
return result.scalar_one_or_none()
@staticmethod
@@ -245,8 +263,20 @@ class MasterDao:
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_site(cls, db: AsyncSession, site_id: int) -> BizCustomerSite | None:
return await cls._get(db, BizCustomerSite, BizCustomerSite.site_id, site_id)
async def get_site(
cls,
db: AsyncSession,
site_id: int,
*,
for_update: bool = False,
) -> BizCustomerSite | None:
return await cls._get(
db,
BizCustomerSite,
BizCustomerSite.site_id,
site_id,
for_update=for_update,
)
@classmethod
async def find_site_by_code(
@@ -343,8 +373,20 @@ class MasterDao:
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_warehouse(cls, db: AsyncSession, warehouse_id: int) -> BizWarehouse | None:
return await cls._get(db, BizWarehouse, BizWarehouse.warehouse_id, warehouse_id)
async def get_warehouse(
cls,
db: AsyncSession,
warehouse_id: int,
*,
for_update: bool = False,
) -> BizWarehouse | None:
return await cls._get(
db,
BizWarehouse,
BizWarehouse.warehouse_id,
warehouse_id,
for_update=for_update,
)
@classmethod
async def find_warehouse_by_code(cls, db: AsyncSession, warehouse_code: str) -> BizWarehouse | None:
@@ -391,6 +433,37 @@ class MasterDao:
result = await db.execute(statement)
return result.scalar_one_or_none()
@classmethod
async def get_pallet_removability(
cls,
db: AsyncSession,
pallet_ids: list[int],
) -> dict[int, bool]:
"""批量判断托盘是否仍为未入库且没有任何业务引用。"""
removability = dict.fromkeys(pallet_ids, False)
if not pallet_ids:
return removability
has_unstocked_inventory = (
select(BizPalletInventory.pallet_id)
.where(
BizPalletInventory.pallet_id == BizPallet.pallet_id,
BizPalletInventory.flow_state == PalletFlowState.UNSTOCKED.value,
)
.exists()
)
has_no_business_references = tuple(
~select(column).where(column == BizPallet.pallet_id).exists() for column in _PALLET_REMOVAL_BLOCKING_COLUMNS
)
statement = select(
BizPallet.pallet_id,
and_(has_unstocked_inventory, *has_no_business_references).label('can_remove'),
).where(BizPallet.pallet_id.in_(pallet_ids))
result = await db.execute(statement)
for pallet_id, can_remove in result.all():
removability[pallet_id] = bool(can_remove)
return removability
@classmethod
async def find_pallet_by_code(cls, db: AsyncSession, pallet_code: str) -> BizPallet | None:
return await cls._find(db, BizPallet, BizPallet.pallet_code, pallet_code)
@@ -484,6 +557,16 @@ class MasterDao:
)
return list(result.all())
@classmethod
async def list_warehouse_assigned_user_ids(cls, db: AsyncSession, warehouse_id: int) -> list[int]:
result = await db.execute(select(BizUserWarehouse.user_id).where(BizUserWarehouse.warehouse_id == warehouse_id))
return list(result.scalars().all())
@classmethod
async def list_site_assigned_user_ids(cls, db: AsyncSession, site_id: int) -> list[int]:
result = await db.execute(select(BizUserSite.user_id).where(BizUserSite.site_id == site_id))
return list(result.scalars().all())
@classmethod
async def count_references(
cls,