修复:限定幂等键精确业务域

This commit is contained in:
焦龙言
2026-07-27 18:54:50 +08:00
parent 4a321098f8
commit b6f3b1e1f0
18 changed files with 730 additions and 135 deletions
@@ -11,7 +11,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import FastAPI
from fastapi.routing import APIRoute
from sqlalchemy.dialects import postgresql
from sqlalchemy.exc import IntegrityError
from sqlalchemy.exc import IntegrityError, MultipleResultsFound
from sqlalchemy.ext.asyncio import AsyncSession
from common.aspect.interface_auth import CheckUserInterfaceAuth
@@ -2181,6 +2181,96 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
parent_lock.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_same_key_retry_uses_exact_route_when_user_can_access_duplicate_keys(self) -> None:
target = make_shipment(
status='CONFIRMED',
idempotency_key='confirm-key',
request_hash=self.request_hash,
)
other_domain = make_shipment(
13,
source_warehouse_id=8,
target_site_id=10,
status='CONFIRMED',
idempotency_key='confirm-key',
request_hash=ShipmentService._build_confirm_request_hash(13),
)
async def read_key(
_db: AsyncSession,
_key: str,
*,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> BizShipmentOrder | None:
matches = [
shipment
for shipment in (target, other_domain)
if (warehouse_ids is None or shipment.source_warehouse_id in warehouse_ids)
and (site_ids is None or shipment.target_site_id in site_ids)
]
if len(matches) > 1:
raise MultipleResultsFound('广域幂等键查询返回多条发货单')
return matches[0] if matches else None
get_detail = AsyncMock(return_value=target)
key_read = AsyncMock(side_effect=read_key)
with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7, 8])),
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9, 10])),
patch.object(OperationDao, 'get_shipment_by_id', new=get_detail),
patch.object(OperationDao, 'get_shipment_by_confirm_idempotency_key', new=key_read),
patch.object(OperationDao, 'get_shipment_for_update', new=AsyncMock(return_value=target)),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(
return_value=BizWarehouse(
warehouse_id=7,
warehouse_code='W-7',
warehouse_name='七号仓',
status='0',
)
),
),
patch.object(
MasterDao,
'get_site',
new=AsyncMock(
return_value=BizCustomerSite(
site_id=9,
customer_id=1,
site_code='S-9',
site_name='九号现场',
status='0',
)
),
),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
):
result = await ShipmentService.confirm_shipment_services(
self.db,
12,
self.command,
self.user,
)
self.assertTrue(result.is_success)
get_detail.assert_awaited_once_with(
self.db,
12,
warehouse_ids=[7, 8],
site_ids=[9, 10],
)
key_read.assert_awaited_once_with(
self.db,
'confirm-key',
warehouse_ids=[7],
site_ids=[9],
)
self.db.rollback.assert_awaited_once()
async def test_integrity_recovery_nonexact_result_is_stable_conflict_not_success(self) -> None:
initial = make_shipment()
nonexact = make_shipment(
@@ -2413,6 +2503,113 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(2, self.db.rollback.await_count)
self.db.commit.assert_not_awaited()
async def test_integrity_recovery_uses_exact_route_with_duplicate_visible_keys(self) -> None:
initial = make_shipment(status='DRAFT')
recovered = make_shipment(
status='CONFIRMED',
idempotency_key='confirm-key',
request_hash=self.request_hash,
)
other_domain = make_shipment(
13,
source_warehouse_id=8,
target_site_id=10,
status='CONFIRMED',
idempotency_key='confirm-key',
request_hash=ShipmentService._build_confirm_request_hash(13),
)
read_count = 0
async def read_key(
_db: AsyncSession,
_key: str,
*,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> BizShipmentOrder | None:
nonlocal read_count
read_count += 1
if read_count == 1:
return None
matches = [
shipment
for shipment in (recovered, other_domain)
if (warehouse_ids is None or shipment.source_warehouse_id in warehouse_ids)
and (site_ids is None or shipment.target_site_id in site_ids)
]
if len(matches) > 1:
raise MultipleResultsFound('广域幂等键恢复查询返回多条发货单')
return matches[0] if matches else None
warehouse_scope = AsyncMock(side_effect=[[7, 8], [7, 8]])
site_scope = AsyncMock(side_effect=[[9, 10], [9, 10]])
key_read = AsyncMock(side_effect=read_key)
parent_lock = AsyncMock(side_effect=[integrity_error(), recovered])
with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=warehouse_scope),
patch.object(AccessDao, 'list_user_site_ids', new=site_scope),
patch.object(OperationDao, 'get_shipment_by_id', new=AsyncMock(return_value=initial)),
patch.object(OperationDao, 'get_shipment_by_confirm_idempotency_key', new=key_read),
patch.object(OperationDao, 'get_shipment_for_update', new=parent_lock),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(
return_value=BizWarehouse(
warehouse_id=7,
warehouse_code='W-7',
warehouse_name='七号仓',
status='0',
)
),
),
patch.object(
MasterDao,
'get_site',
new=AsyncMock(
return_value=BizCustomerSite(
site_id=9,
customer_id=1,
site_code='S-9',
site_name='九号现场',
status='0',
)
),
),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
):
result = await ShipmentService.confirm_shipment_services(
self.db,
12,
self.command,
self.user,
)
self.assertTrue(result.is_success)
self.assertEqual(2, warehouse_scope.await_count)
self.assertEqual(2, site_scope.await_count)
self.assertEqual(2, parent_lock.await_count)
self.assertEqual(
[
unittest.mock.call(
self.db,
'confirm-key',
warehouse_ids=[7],
site_ids=[9],
),
unittest.mock.call(
self.db,
'confirm-key',
warehouse_ids=[7],
site_ids=[9],
),
],
key_read.await_args_list,
)
self.assertEqual(2, self.db.rollback.await_count)
self.db.commit.assert_not_awaited()
async def test_revoked_mapping_after_locks_hides_document_and_mutates_nothing(self) -> None:
shipment = make_shipment(pallet_ids=(2,))
inventory = make_inventory(2, version=4)