修复:补齐测试批次恢复锁定
This commit is contained in:
@@ -160,8 +160,8 @@ async def edit_business_inbound(
|
||||
|
||||
@inbound_controller.delete(
|
||||
'/{inbound_ids}',
|
||||
summary='删除首次入库草稿接口',
|
||||
description='原子删除一个或多个尚未确认的首次入库草稿',
|
||||
summary='不支持单条物理删除,请通过测试数据批次清理',
|
||||
description='不支持单条物理删除,请通过测试数据批次清理',
|
||||
response_model=ResponseBaseModel,
|
||||
dependencies=[UserInterfaceAuthDependency('business:inbound:remove')],
|
||||
)
|
||||
|
||||
@@ -177,8 +177,8 @@ async def edit_business_shipment(
|
||||
|
||||
@shipment_controller.delete(
|
||||
'/{shipment_ids}',
|
||||
summary='删除整托发货草稿接口',
|
||||
description='原子删除一个或多个尚未确认的整托发货草稿',
|
||||
summary='不支持单条物理删除,请通过测试数据批次清理',
|
||||
description='不支持单条物理删除,请通过测试数据批次清理',
|
||||
response_model=ResponseBaseModel,
|
||||
dependencies=[UserInterfaceAuthDependency('business:shipment:remove')],
|
||||
)
|
||||
|
||||
@@ -112,6 +112,7 @@ class InventoryDao:
|
||||
db: AsyncSession,
|
||||
inventory: BizPalletInventory,
|
||||
event: BizPalletEvent,
|
||||
scope: TestBatchScope,
|
||||
) -> BizPalletEvent:
|
||||
"""在调用方持有库存行锁时,为托盘追加下一序号事件。"""
|
||||
last_sequence = 0
|
||||
@@ -121,6 +122,7 @@ class InventoryDao:
|
||||
.where(
|
||||
BizPalletEvent.event_id == inventory.last_event_id,
|
||||
BizPalletEvent.pallet_id == inventory.pallet_id,
|
||||
batch_scope_condition(BizPalletEvent.test_batch_id, scope),
|
||||
)
|
||||
.with_for_update()
|
||||
)
|
||||
|
||||
@@ -304,6 +304,7 @@ class FillingService:
|
||||
test_scope: TestBatchScope,
|
||||
) -> CrudResponseModel:
|
||||
try:
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
|
||||
if warehouse_ids is not None and page_object.warehouse_id not in warehouse_ids:
|
||||
raise PermissionException(data='', message='无权访问该仓库')
|
||||
@@ -509,7 +510,7 @@ class FillingService:
|
||||
request_hash=request_hash,
|
||||
test_batch_id=test_scope.batch_id,
|
||||
)
|
||||
await InventoryDao.add_event(query_db, inventory, event)
|
||||
await InventoryDao.add_event(query_db, inventory, event, test_scope)
|
||||
await query_db.commit()
|
||||
return cls._success(filling_id)
|
||||
except IntegrityError:
|
||||
|
||||
@@ -502,6 +502,7 @@ class InboundService:
|
||||
test_scope: TestBatchScope,
|
||||
) -> CrudResponseModel:
|
||||
try:
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
|
||||
current = await OperationDao.get_inbound_by_id(
|
||||
query_db,
|
||||
@@ -694,7 +695,7 @@ class InboundService:
|
||||
request_hash=request_hash,
|
||||
test_batch_id=test_scope.batch_id,
|
||||
)
|
||||
await InventoryDao.add_event(query_db, inventory, event)
|
||||
await InventoryDao.add_event(query_db, inventory, event, test_scope)
|
||||
|
||||
await query_db.commit()
|
||||
return cls._success('确认成功')
|
||||
|
||||
@@ -585,7 +585,6 @@ class ReturnService:
|
||||
) -> None:
|
||||
site = await MasterDao.get_site(query_db, return_order.source_site_id, test_scope, for_update=True)
|
||||
try:
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
await AccessService.assert_site_access(
|
||||
query_db,
|
||||
current_user,
|
||||
@@ -607,6 +606,7 @@ class ReturnService:
|
||||
test_scope: TestBatchScope,
|
||||
) -> CrudResponseModel:
|
||||
try:
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
site_ids = await cls._site_scope(query_db, current_user)
|
||||
request_warehouse_ids = None if site_ids is None else []
|
||||
recovered = None
|
||||
@@ -678,6 +678,7 @@ class ReturnService:
|
||||
preflight_existing_id = int(existing.return_id) if existing is not None else None
|
||||
|
||||
await query_db.rollback()
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
if preflight_existing_id is not None:
|
||||
locked_existing = await OperationDao.get_return_for_update(
|
||||
query_db,
|
||||
@@ -802,7 +803,7 @@ class ReturnService:
|
||||
request_hash=request_hash,
|
||||
test_batch_id=test_scope.batch_id,
|
||||
)
|
||||
await InventoryDao.add_event(query_db, inventory, event)
|
||||
await InventoryDao.add_event(query_db, inventory, event, test_scope)
|
||||
|
||||
await query_db.commit()
|
||||
return cls._success('申请成功', return_id)
|
||||
@@ -813,6 +814,7 @@ class ReturnService:
|
||||
page_object,
|
||||
cls._build_request_hash(page_object),
|
||||
current_user,
|
||||
test_scope,
|
||||
)
|
||||
except Exception:
|
||||
await query_db.rollback()
|
||||
@@ -941,6 +943,7 @@ class ReturnService:
|
||||
test_scope: TestBatchScope,
|
||||
) -> CrudResponseModel:
|
||||
try:
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
|
||||
confirm_site_ids = None if warehouse_ids is None else []
|
||||
route = await OperationDao.get_return_by_id(
|
||||
@@ -1040,6 +1043,7 @@ class ReturnService:
|
||||
raise ServiceException(message='确认退回失败,幂等键已被其他请求使用')
|
||||
|
||||
await query_db.rollback()
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
locked = await OperationDao.get_return_for_update(
|
||||
query_db,
|
||||
return_id,
|
||||
@@ -1146,7 +1150,7 @@ class ReturnService:
|
||||
request_hash=request_hash,
|
||||
test_batch_id=test_scope.batch_id,
|
||||
)
|
||||
await InventoryDao.add_event(query_db, inventory, event)
|
||||
await InventoryDao.add_event(query_db, inventory, event, test_scope)
|
||||
|
||||
await query_db.commit()
|
||||
return cls._success('确认成功', return_id)
|
||||
|
||||
@@ -738,6 +738,7 @@ class ShipmentService:
|
||||
test_scope: TestBatchScope,
|
||||
) -> CrudResponseModel:
|
||||
try:
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
|
||||
current = await OperationDao.get_shipment_by_id(
|
||||
query_db,
|
||||
@@ -932,7 +933,7 @@ class ShipmentService:
|
||||
request_hash=request_hash,
|
||||
test_batch_id=test_scope.batch_id,
|
||||
)
|
||||
await InventoryDao.add_event(query_db, inventory, event)
|
||||
await InventoryDao.add_event(query_db, inventory, event, test_scope)
|
||||
|
||||
await query_db.commit()
|
||||
return cls._success('确认成功')
|
||||
|
||||
@@ -177,6 +177,7 @@ class UsageService:
|
||||
test_scope: TestBatchScope,
|
||||
) -> CrudResponseModel:
|
||||
try:
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
site_ids = await cls._site_scope(query_db, current_user)
|
||||
if site_ids is not None and expected_site_id not in site_ids:
|
||||
cls._raise_pallet_not_accessible(current_user)
|
||||
@@ -403,6 +404,7 @@ class UsageService:
|
||||
cls._raise_pallet_not_accessible(current_user)
|
||||
scanned_pallet_id = int(scanned_pallet.pallet_id)
|
||||
await query_db.rollback()
|
||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||
|
||||
inventory = await InventoryDao.get_inventory_for_update(
|
||||
query_db,
|
||||
@@ -579,7 +581,7 @@ class UsageService:
|
||||
request_hash=request_hash,
|
||||
test_batch_id=test_scope.batch_id,
|
||||
)
|
||||
await InventoryDao.add_event(query_db, inventory, event)
|
||||
await InventoryDao.add_event(query_db, inventory, event, test_scope)
|
||||
await query_db.commit()
|
||||
return result
|
||||
except IntegrityError:
|
||||
|
||||
@@ -446,6 +446,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
||||
_db: AsyncSession,
|
||||
inventory: BizPalletInventory,
|
||||
event: BizPalletEvent,
|
||||
_scope: TestBatchScope,
|
||||
) -> BizPalletEvent:
|
||||
actions.append('event-flush')
|
||||
event.event_id = 91
|
||||
|
||||
@@ -1092,6 +1092,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
_db: AsyncSession,
|
||||
inventory: BizPalletInventory,
|
||||
event: BizPalletEvent,
|
||||
_scope: TestBatchScope,
|
||||
) -> BizPalletEvent:
|
||||
actions.append(f'event-{inventory.pallet_id}')
|
||||
events.append(event)
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestDaoPublicContract(unittest.TestCase):
|
||||
'get_pallet_by_qr_token': ('db', 'qr_token', 'scope'),
|
||||
'get_pallets_by_ids': ('db', 'pallet_ids', 'scope'),
|
||||
'get_event_by_idempotency_key': ('db', 'pallet_id', 'idempotency_key', 'scope'),
|
||||
'add_event': ('db', 'inventory', 'event'),
|
||||
'add_event': ('db', 'inventory', 'event', 'scope'),
|
||||
}
|
||||
for method_name, parameter_names in expected.items():
|
||||
with self.subTest(method=method_name):
|
||||
@@ -401,7 +401,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.db.flush.side_effect = assign_event_id
|
||||
|
||||
actual = await InventoryDao.add_event(self.db, inventory, event)
|
||||
actual = await InventoryDao.add_event(self.db, inventory, event, TestBatchScope.formal())
|
||||
|
||||
self.assertIs(event, actual)
|
||||
self.assertEqual(7, event.pallet_id)
|
||||
@@ -417,6 +417,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertNotIn('max(', sql.lower())
|
||||
self.assertIn('biz_pallet_event.event_id = 40', sql)
|
||||
self.assertIn('biz_pallet_event.pallet_id = 7', sql)
|
||||
self.assertIn('biz_pallet_event.test_batch_id IS NULL', sql)
|
||||
self.assertIn('FOR UPDATE', sql)
|
||||
self.assertNotIn('ORDER BY', sql)
|
||||
self.assertNotIn('LIMIT', sql)
|
||||
@@ -429,7 +430,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
|
||||
inventory = BizPalletInventory(pallet_id=8, last_event_id=None)
|
||||
event = BizPalletEvent(pallet_id=8, idempotency_key='first-key', event_id=51)
|
||||
|
||||
await InventoryDao.add_event(self.db, inventory, event)
|
||||
await InventoryDao.add_event(self.db, inventory, event, TestBatchScope.formal())
|
||||
|
||||
self.assertEqual(1, event.event_seq)
|
||||
self.assertEqual(51, inventory.last_event_id)
|
||||
@@ -445,7 +446,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
|
||||
event = BizPalletEvent(pallet_id=8, idempotency_key='broken-pointer')
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, '托盘库存事件指针不一致'):
|
||||
await InventoryDao.add_event(self.db, inventory, event)
|
||||
await InventoryDao.add_event(self.db, inventory, event, TestBatchScope.formal())
|
||||
|
||||
self.db.execute.assert_awaited_once()
|
||||
self.db.add.assert_not_called()
|
||||
|
||||
@@ -24,7 +24,7 @@ from module_business.dao.access_dao import AccessDao
|
||||
from module_business.dao.inventory_dao import InventoryDao
|
||||
from module_business.dao.master_dao import MasterDao
|
||||
from module_business.dao.operation_dao import OperationDao
|
||||
from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus
|
||||
from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus, TestBatchStatus
|
||||
from module_business.domain.test_batch_scope import TestBatchScope
|
||||
from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizWarehouse
|
||||
from module_business.entity.do.operation_do import (
|
||||
@@ -44,6 +44,7 @@ from module_business.entity.vo.operation_vo import (
|
||||
ReturnScanQueryModel,
|
||||
)
|
||||
from module_business.service.access_service import AccessService
|
||||
from module_business.service.test_batch_access_service import TestBatchAccessService
|
||||
|
||||
RETURN_SERVICE_SPEC = importlib.util.find_spec('module_business.service.return_service')
|
||||
RETURN_CONTROLLER_SPEC = importlib.util.find_spec('module_business.controller.return_controller')
|
||||
@@ -741,6 +742,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
||||
_db: AsyncSession,
|
||||
_inventory: BizPalletInventory,
|
||||
event: BizPalletEvent,
|
||||
_scope: TestBatchScope,
|
||||
) -> BizPalletEvent:
|
||||
captured_events.append(event)
|
||||
return event
|
||||
@@ -790,6 +792,48 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
||||
warehouse_access.assert_not_awaited()
|
||||
self.db.commit.assert_awaited_once()
|
||||
|
||||
async def test_request_integrity_error_recovers_same_test_batch_idempotently(self) -> None:
|
||||
scope = TestBatchScope(batch_id=4, mode='TEST', status=TestBatchStatus.ACTIVE, writable=True)
|
||||
winner = make_return_order(request_hash=ReturnService._build_request_hash(self.page))
|
||||
winner.test_batch_id = 4
|
||||
for item in winner.items:
|
||||
item.test_batch_id = 4
|
||||
inventories = [
|
||||
make_inventory(2),
|
||||
make_inventory(5, remaining=0, state=PalletFlowState.AT_SITE_EMPTY.value),
|
||||
]
|
||||
pallets = [make_pallet(2), make_pallet(5)]
|
||||
for inventory in inventories:
|
||||
inventory.test_batch_id = 4
|
||||
for pallet in pallets:
|
||||
pallet.test_batch_id = 4
|
||||
filling = make_filling(2)
|
||||
filling.test_batch_id = 4
|
||||
key_read = AsyncMock(side_effect=[None, None, winner])
|
||||
order_lock = AsyncMock(return_value=winner)
|
||||
batch_lock = AsyncMock()
|
||||
|
||||
with (
|
||||
patch.object(TestBatchAccessService, 'lock_active_batch', new=batch_lock),
|
||||
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9])),
|
||||
patch.object(OperationDao, 'get_return_by_request_idempotency_key', new=key_read),
|
||||
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=inventories)),
|
||||
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=pallets)),
|
||||
patch.object(OperationDao, 'get_active_filling_for_update', new=AsyncMock(return_value=filling)),
|
||||
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
||||
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
||||
patch.object(OperationDao, 'add_return_order', new=AsyncMock(side_effect=integrity_error())),
|
||||
):
|
||||
result = await ReturnService.request_return_services(self.db, self.page, self.user, scope)
|
||||
|
||||
self.assertTrue(result.is_success)
|
||||
self.assertEqual(13, result.result)
|
||||
self.assertEqual(3, batch_lock.await_count)
|
||||
order_lock.assert_awaited_once_with(self.db, 13, scope, site_ids=[9], warehouse_ids=[])
|
||||
self.assertEqual(3, self.db.rollback.await_count)
|
||||
|
||||
async def test_concurrent_same_request_returns_winner_after_inventory_wait(self) -> None: # noqa: PLR0915
|
||||
request_hash = ReturnService._build_request_hash(self.page)
|
||||
winner = make_return_order(request_hash=request_hash)
|
||||
@@ -1265,6 +1309,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
||||
_db: AsyncSession,
|
||||
_inventory: BizPalletInventory,
|
||||
event: BizPalletEvent,
|
||||
_scope: TestBatchScope,
|
||||
) -> BizPalletEvent:
|
||||
events.append(event)
|
||||
return event
|
||||
@@ -1595,6 +1640,7 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
||||
_db: AsyncSession,
|
||||
_inventory: BizPalletInventory,
|
||||
event: BizPalletEvent,
|
||||
_scope: TestBatchScope,
|
||||
) -> BizPalletEvent:
|
||||
events.append(event)
|
||||
return event
|
||||
|
||||
@@ -1626,6 +1626,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
|
||||
_db: AsyncSession,
|
||||
inventory: BizPalletInventory,
|
||||
event: BizPalletEvent,
|
||||
_scope: TestBatchScope,
|
||||
) -> BizPalletEvent:
|
||||
actions.append(f'event-{inventory.pallet_id}')
|
||||
event.event_id = 200 + inventory.pallet_id
|
||||
|
||||
@@ -884,6 +884,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
_db: AsyncSession,
|
||||
inventory: BizPalletInventory,
|
||||
event: BizPalletEvent,
|
||||
_scope: TestBatchScope,
|
||||
) -> BizPalletEvent:
|
||||
actions.append('event-flush')
|
||||
event.event_id = 91
|
||||
|
||||
Reference in New Issue
Block a user