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

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
@@ -1,7 +1,7 @@
import secrets
from datetime import datetime
from io import BytesIO
from typing import Any, Literal
from typing import Literal
import qrcode
from sqlalchemy.exc import IntegrityError
@@ -11,7 +11,6 @@ from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from module_business.dao.access_dao import AccessDao
from module_business.dao.master_dao import MasterDao
from module_business.domain.enums import PalletFlowState
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import BizPalletInventory
from module_business.entity.vo.master_vo import (
@@ -58,18 +57,6 @@ class PalletService:
raise ServiceException(message='托盘ID不能重复')
return sorted(pallet_ids)
@classmethod
async def _can_remove(cls, db: AsyncSession, pallet_id: int) -> bool:
inventory = await MasterDao.get_pallet_inventory(db, pallet_id)
if inventory is None or inventory.flow_state != PalletFlowState.UNSTOCKED.value:
return False
return await MasterDao.count_pallet_events(db, pallet_id) == 0
@classmethod
async def _to_pallet_model(cls, db: AsyncSession, record: Any) -> PalletModel:
model = PalletModel.model_validate(record)
return model.model_copy(update={'can_remove': await cls._can_remove(db, model.pallet_id)})
@classmethod
async def _lock_pallet_inventory(cls, db: AsyncSession, pallet_id: int) -> tuple[BizPallet, BizPalletInventory]:
"""按库存、托盘顺序加锁,避免与流转事务形成反序死锁。"""
@@ -91,17 +78,25 @@ class PalletService:
is_page: bool = True,
) -> PageModel | list[PalletModel]:
result = await MasterDao.list_pallet(db, query_object, is_page)
records = result.rows if isinstance(result, PageModel) else result
models = [PalletModel.model_validate(record) for record in records]
removability = await MasterDao.get_pallet_removability(
db,
[model.pallet_id for model in models],
)
rows = [model.model_copy(update={'can_remove': removability.get(model.pallet_id, False)}) for model in models]
if isinstance(result, PageModel):
rows = [await cls._to_pallet_model(db, row) for row in result.rows]
return result.model_copy(update={'rows': rows})
return [await cls._to_pallet_model(db, row) for row in result]
return rows
@classmethod
async def detail_pallet(cls, db: AsyncSession, pallet_id: int) -> PalletModel:
pallet = await MasterDao.get_pallet(db, pallet_id)
if pallet is None:
raise ServiceException(message='托盘不存在')
return await cls._to_pallet_model(db, pallet)
model = PalletModel.model_validate(pallet)
removability = await MasterDao.get_pallet_removability(db, [pallet_id])
return model.model_copy(update={'can_remove': removability.get(pallet_id, False)})
@classmethod
async def add_pallet(
@@ -179,11 +174,11 @@ class PalletService:
try:
pallet_ids = cls._extract_pallet_ids(value)
for pallet_id in pallet_ids:
_, inventory = await cls._lock_pallet_inventory(db, pallet_id)
if inventory.flow_state != PalletFlowState.UNSTOCKED.value:
raise ServiceException(message='托盘未处于未入库状态,不能删除')
if await MasterDao.count_pallet_events(db, pallet_id) > 0:
raise ServiceException(message='托盘已有流转事件,不能删除')
await cls._lock_pallet_inventory(db, pallet_id)
removability = await MasterDao.get_pallet_removability(db, pallet_ids)
if not all(removability.get(pallet_id, False) for pallet_id in pallet_ids):
raise ServiceException(message='托盘状态或关联数据不允许删除')
for pallet_id in pallet_ids:
await MasterDao.delete_pallet_inventory(db, pallet_id)
@@ -208,19 +203,16 @@ class PalletService:
raise ServiceException(message=f'{_ASSIGNMENT_LABELS[resource]}不存在')
users = await MasterDao.list_assignable_users(db)
result = []
association_method = getattr(AccessDao, f'list_user_{resource}_ids')
for user in users:
assigned_ids = await association_method(db, user.user_id)
result.append(
BusinessUserOptionModel(
user_id=user.user_id,
user_name=user.user_name,
nick_name=user.nick_name,
assigned=resource_id in assigned_ids,
)
assigned_user_ids = set(await getattr(MasterDao, f'list_{resource}_assigned_user_ids')(db, resource_id))
return [
BusinessUserOptionModel(
user_id=user.user_id,
user_name=user.user_name,
nick_name=user.nick_name,
assigned=user.user_id in assigned_user_ids,
)
return result
for user in users
]
@classmethod
async def list_warehouse_users(
@@ -248,7 +240,14 @@ class PalletService:
) -> CrudResponseModel:
label = _ASSIGNMENT_LABELS[resource]
try:
if await getattr(MasterDao, f'get_{resource}')(db, resource_id) is None:
if (
await getattr(MasterDao, f'get_{resource}')(
db,
resource_id,
for_update=True,
)
is None
):
raise ServiceException(message=f'{label}不存在')
users = await MasterDao.list_assignable_users_by_ids(db, assignment.user_ids)
if {user.user_id for user in users} != set(assignment.user_ids):