功能:隔离五类测试业务链路

This commit is contained in:
焦龙言
2026-09-23 08:27:14 +08:00
parent 43fe17cc70
commit f5df2150d8
18 changed files with 1206 additions and 831 deletions
@@ -11,6 +11,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import (
BusinessOptionModel,
FillingCreateModel,
@@ -40,11 +42,13 @@ async def get_business_filling_list(
filling_page_query: Annotated[FillingPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await FillingService.get_filling_list_services(
query_db,
filling_page_query,
current_user,
test_scope,
is_page=True,
)
return ResponseUtil.success(model_content=result)
@@ -61,8 +65,9 @@ async def get_business_filling_strain_options(
request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await FillingService.get_strain_options_services(query_db, current_user)
result = await FillingService.get_strain_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -81,11 +86,13 @@ async def scan_business_filling_pallet(
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await FillingService.scan_filling_pallet_services(
query_db,
qr_token,
current_user,
test_scope,
)
return ResponseUtil.success(data=result)
@@ -102,8 +109,9 @@ async def query_detail_business_filling(
filling_id: Annotated[int, Path(gt=0, description='装填记录ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await FillingService.filling_detail_services(query_db, filling_id, current_user)
result = await FillingService.filling_detail_services(query_db, filling_id, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -120,6 +128,7 @@ async def add_business_filling(
add_filling: FillingCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await FillingService.fill_pallet_services(query_db, add_filling, current_user)
result = await FillingService.fill_pallet_services(query_db, add_filling, current_user, test_scope)
return ResponseUtil.success(msg=result.message)
@@ -11,6 +11,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import (
BusinessOptionModel,
ConfirmCommandModel,
@@ -43,11 +45,13 @@ async def get_business_inbound_list(
inbound_page_query: Annotated[InboundPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await InboundService.get_inbound_list_services(
query_db,
inbound_page_query,
current_user,
test_scope,
is_page=True,
)
return ResponseUtil.success(model_content=result)
@@ -64,8 +68,9 @@ async def get_business_inbound_warehouse_options(
request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await InboundService.get_warehouse_options_services(query_db, current_user)
result = await InboundService.get_warehouse_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -85,12 +90,14 @@ async def scan_business_inbound_pallet(
inbound_scan_query: Annotated[InboundScanQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await InboundService.scan_inbound_pallet_services(
query_db,
qr_token,
inbound_scan_query,
current_user,
test_scope,
)
return ResponseUtil.success(data=result)
@@ -107,8 +114,9 @@ async def query_detail_business_inbound(
inbound_id: Annotated[int, Path(gt=0, description='入库单ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await InboundService.inbound_detail_services(query_db, inbound_id, current_user)
result = await InboundService.inbound_detail_services(query_db, inbound_id, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -125,8 +133,9 @@ async def add_business_inbound(
add_inbound: InboundCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await InboundService.add_inbound_services(query_db, add_inbound, current_user)
result = await InboundService.add_inbound_services(query_db, add_inbound, current_user, test_scope)
return ResponseUtil.success(msg=result.message)
@@ -143,8 +152,9 @@ async def edit_business_inbound(
edit_inbound: InboundUpdateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await InboundService.edit_inbound_services(query_db, edit_inbound, current_user)
result = await InboundService.edit_inbound_services(query_db, edit_inbound, current_user, test_scope)
return ResponseUtil.success(msg=result.message)
@@ -164,11 +174,13 @@ async def delete_business_inbound(
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await InboundService.delete_inbound_services(
query_db,
[int(inbound_id) for inbound_id in inbound_ids.split(',')],
current_user,
test_scope,
)
return ResponseUtil.success(msg=result.message)
@@ -187,11 +199,13 @@ async def confirm_business_inbound(
confirm_command: ConfirmCommandModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await InboundService.confirm_inbound_services(
query_db,
inbound_id,
confirm_command,
current_user,
test_scope,
)
return ResponseUtil.success(msg=result.message)
@@ -13,6 +13,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import (
BusinessOptionModel,
PalletScanResponseModel,
@@ -44,11 +46,13 @@ async def get_business_return_list(
return_page_query: Annotated[ReturnPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ReturnService.get_return_list_services(
query_db,
return_page_query,
current_user,
test_scope,
is_page=True,
)
return ResponseUtil.success(model_content=result)
@@ -65,8 +69,9 @@ async def get_business_return_warehouse_options(
request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ReturnService.get_warehouse_options_services(query_db, current_user)
result = await ReturnService.get_warehouse_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -86,12 +91,14 @@ async def scan_business_return_pallet(
return_scan_query: Annotated[ReturnScanQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ReturnService.scan_return_pallet_services(
query_db,
qr_token,
return_scan_query,
current_user,
test_scope,
)
return ResponseUtil.success(data=result)
@@ -109,8 +116,9 @@ async def request_business_return(
request_command: ReturnRequestModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await ReturnService.request_return_services(query_db, request_command, current_user)
result = await ReturnService.request_return_services(query_db, request_command, current_user, test_scope)
return ResponseUtil.success(msg=result.message)
@@ -126,8 +134,9 @@ async def query_detail_business_return(
return_id: Annotated[int, Path(gt=0, description='退回单ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ReturnService.return_detail_services(query_db, return_id, current_user)
result = await ReturnService.return_detail_services(query_db, return_id, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -145,11 +154,13 @@ async def confirm_business_return(
confirm_command: ReturnConfirmModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await ReturnService.confirm_return_services(
query_db,
return_id,
confirm_command,
current_user,
test_scope,
)
return ResponseUtil.success(msg=result.message)
@@ -11,6 +11,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import (
BusinessOptionModel,
ConfirmCommandModel,
@@ -43,11 +45,13 @@ async def get_business_shipment_list(
shipment_page_query: Annotated[ShipmentPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ShipmentService.get_shipment_list_services(
query_db,
shipment_page_query,
current_user,
test_scope,
is_page=True,
)
return ResponseUtil.success(model_content=result)
@@ -64,8 +68,9 @@ async def get_business_shipment_warehouse_options(
request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ShipmentService.get_warehouse_options_services(query_db, current_user)
result = await ShipmentService.get_warehouse_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -80,8 +85,9 @@ async def get_business_shipment_site_options(
request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ShipmentService.get_site_options_services(query_db, current_user)
result = await ShipmentService.get_site_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -101,12 +107,14 @@ async def scan_business_shipment_pallet(
shipment_scan_query: Annotated[ShipmentScanQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ShipmentService.scan_shipment_pallet_services(
query_db,
qr_token,
shipment_scan_query,
current_user,
test_scope,
)
return ResponseUtil.success(data=result)
@@ -123,8 +131,9 @@ async def query_detail_business_shipment(
shipment_id: Annotated[int, Path(gt=0, description='发货单ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await ShipmentService.shipment_detail_services(query_db, shipment_id, current_user)
result = await ShipmentService.shipment_detail_services(query_db, shipment_id, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -141,8 +150,9 @@ async def add_business_shipment(
add_shipment: ShipmentCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await ShipmentService.add_shipment_services(query_db, add_shipment, current_user)
result = await ShipmentService.add_shipment_services(query_db, add_shipment, current_user, test_scope)
return ResponseUtil.success(msg=result.message)
@@ -159,8 +169,9 @@ async def edit_business_shipment(
edit_shipment: ShipmentUpdateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await ShipmentService.edit_shipment_services(query_db, edit_shipment, current_user)
result = await ShipmentService.edit_shipment_services(query_db, edit_shipment, current_user, test_scope)
return ResponseUtil.success(msg=result.message)
@@ -180,11 +191,13 @@ async def delete_business_shipment(
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await ShipmentService.delete_shipment_services(
query_db,
[int(shipment_id) for shipment_id in shipment_ids.split(',')],
current_user,
test_scope,
)
return ResponseUtil.success(msg=result.message)
@@ -203,11 +216,13 @@ async def confirm_business_shipment(
confirm_command: ConfirmCommandModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await ShipmentService.confirm_shipment_services(
query_db,
shipment_id,
confirm_command,
current_user,
test_scope,
)
return ResponseUtil.success(msg=result.message)
@@ -13,6 +13,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import (
BusinessOptionModel,
PalletScanResponseModel,
@@ -43,11 +45,13 @@ async def get_business_usage_list(
usage_page_query: Annotated[UsagePageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await UsageService.get_usage_list_services(
query_db,
usage_page_query,
current_user,
test_scope,
is_page=True,
)
return ResponseUtil.success(model_content=result)
@@ -65,8 +69,9 @@ async def get_business_usage_line_options(
site_id: Annotated[int, Query(alias='siteId', gt=0, description='客户现场ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await UsageService.get_line_options_services(query_db, site_id, current_user)
result = await UsageService.get_line_options_services(query_db, site_id, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -85,8 +90,9 @@ async def scan_business_usage_pallet(
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await UsageService.scan_usage_pallet_services(query_db, qr_token, current_user)
result = await UsageService.scan_usage_pallet_services(query_db, qr_token, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -102,8 +108,9 @@ async def query_detail_business_usage(
usage_id: Annotated[int, Path(gt=0, description='使用记录ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await UsageService.usage_detail_services(query_db, usage_id, current_user)
result = await UsageService.usage_detail_services(query_db, usage_id, current_user, test_scope)
return ResponseUtil.success(data=result)
@@ -120,6 +127,7 @@ async def add_business_usage(
add_usage: UsageCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await UsageService.record_usage_services(query_db, add_usage, current_user)
result = await UsageService.record_usage_services(query_db, add_usage, current_user, test_scope)
return ResponseUtil.success(msg=result.message, data=result.result)
@@ -1,6 +1,7 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import BizPalletEvent, BizPalletInventory
@@ -13,10 +14,14 @@ class InventoryDao:
cls,
db: AsyncSession,
pallet_id: int,
scope: TestBatchScope,
) -> BizPalletInventory | None:
result = await db.execute(
select(BizPalletInventory)
.where(BizPalletInventory.pallet_id == pallet_id)
.where(
BizPalletInventory.pallet_id == pallet_id,
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
)
.with_for_update()
.execution_options(populate_existing=True)
)
@@ -27,6 +32,7 @@ class InventoryDao:
cls,
db: AsyncSession,
pallet_ids: list[int],
scope: TestBatchScope,
) -> list[BizPalletInventory]:
ordered_ids = sorted(set(pallet_ids))
if not ordered_ids:
@@ -34,7 +40,10 @@ class InventoryDao:
result = await db.execute(
select(BizPalletInventory)
.where(BizPalletInventory.pallet_id.in_(ordered_ids))
.where(
BizPalletInventory.pallet_id.in_(ordered_ids),
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
)
.order_by(BizPalletInventory.pallet_id)
.with_for_update()
.execution_options(populate_existing=True)
@@ -46,8 +55,14 @@ class InventoryDao:
cls,
db: AsyncSession,
qr_token: str,
scope: TestBatchScope,
) -> BizPallet | None:
result = await db.execute(select(BizPallet).where(BizPallet.qr_token == qr_token))
result = await db.execute(
select(BizPallet).where(
BizPallet.qr_token == qr_token,
batch_scope_condition(BizPallet.test_batch_id, scope),
)
)
return result.scalar_one_or_none()
@classmethod
@@ -55,6 +70,7 @@ class InventoryDao:
cls,
db: AsyncSession,
pallet_ids: list[int],
scope: TestBatchScope,
) -> list[BizPallet]:
"""在调用方已锁库存后,按托盘 ID 升序锁定资产行。"""
ordered_ids = sorted(set(pallet_ids))
@@ -63,7 +79,10 @@ class InventoryDao:
result = await db.execute(
select(BizPallet)
.where(BizPallet.pallet_id.in_(ordered_ids))
.where(
BizPallet.pallet_id.in_(ordered_ids),
batch_scope_condition(BizPallet.test_batch_id, scope),
)
.order_by(BizPallet.pallet_id)
.with_for_update()
.execution_options(populate_existing=True)
@@ -76,11 +95,13 @@ class InventoryDao:
db: AsyncSession,
pallet_id: int,
idempotency_key: str,
scope: TestBatchScope,
) -> BizPalletEvent | None:
result = await db.execute(
select(BizPalletEvent).where(
BizPalletEvent.pallet_id == pallet_id,
BizPalletEvent.idempotency_key == idempotency_key,
batch_scope_condition(BizPalletEvent.test_batch_id, scope),
)
)
return result.scalar_one_or_none()
@@ -7,6 +7,7 @@ from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel
from module_business.domain.enums import DocumentStatus, PalletFlowState
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import (
BizFillingRecord,
@@ -56,36 +57,49 @@ class OperationDao:
@staticmethod
def _inbound_scope_conditions(
warehouse_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]:
if warehouse_ids is None:
return []
return [batch_scope_condition(BizInboundOrder.test_batch_id, scope)]
scoped_warehouse_ids = sorted(set(warehouse_ids))
return [BizInboundOrder.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()]
return [
batch_scope_condition(BizInboundOrder.test_batch_id, scope),
BizInboundOrder.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false(),
]
@staticmethod
def _filling_scope_conditions(
warehouse_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]:
if warehouse_ids is None:
return []
return [batch_scope_condition(BizFillingRecord.test_batch_id, scope)]
scoped_warehouse_ids = sorted(set(warehouse_ids))
return [BizFillingRecord.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()]
return [
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
BizFillingRecord.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false(),
]
@staticmethod
def _usage_scope_conditions(
site_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]:
if site_ids is None:
return []
return [batch_scope_condition(BizUsageRecord.test_batch_id, scope)]
scoped_site_ids = sorted(set(site_ids))
return [BizUsageRecord.site_id.in_(scoped_site_ids) if scoped_site_ids else false()]
return [
batch_scope_condition(BizUsageRecord.test_batch_id, scope),
BizUsageRecord.site_id.in_(scoped_site_ids) if scoped_site_ids else false(),
]
@staticmethod
def _shipment_scope_conditions(
warehouse_ids: list[int] | None,
site_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]:
conditions = []
conditions = [batch_scope_condition(BizShipmentOrder.test_batch_id, scope)]
if warehouse_ids is not None:
scoped_warehouse_ids = sorted(set(warehouse_ids))
conditions.append(
@@ -100,9 +114,10 @@ class OperationDao:
def _return_scope_conditions(
site_ids: list[int] | None,
warehouse_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]:
if site_ids is None and warehouse_ids is None:
return []
return [batch_scope_condition(BizReturnOrder.test_batch_id, scope)]
visibility_conditions: list[ColumnElement[bool]] = []
if site_ids is not None:
@@ -115,7 +130,7 @@ class OperationDao:
visibility_conditions.append(
BizReturnOrder.target_warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()
)
return [or_(*visibility_conditions)]
return [batch_scope_condition(BizReturnOrder.test_batch_id, scope), or_(*visibility_conditions)]
@staticmethod
async def _paginate(
@@ -180,6 +195,7 @@ class OperationDao:
item_model: type[Any],
item_order_id_column: InstrumentedAttribute,
item_id_column: InstrumentedAttribute,
scope: TestBatchScope,
*order_conditions: ColumnElement[bool],
) -> Any | None:
order = await cls._get_by_id(
@@ -193,7 +209,9 @@ class OperationDao:
return None
item_result = await db.execute(
select(item_model).where(item_order_id_column == order_id).order_by(item_id_column)
select(item_model)
.where(item_order_id_column == order_id, batch_scope_condition(item_model.test_batch_id, scope))
.order_by(item_id_column)
)
order.items = list(item_result.scalars().all())
return order
@@ -260,6 +278,7 @@ class OperationDao:
cls,
db: AsyncSession,
inbound_id: int,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None:
inbound = await cls._get_for_update(
@@ -267,7 +286,7 @@ class OperationDao:
BizInboundOrder,
BizInboundOrder.inbound_id,
inbound_id,
*cls._inbound_scope_conditions(warehouse_ids),
*cls._inbound_scope_conditions(warehouse_ids, scope),
)
if inbound is None:
return None
@@ -275,7 +294,10 @@ class OperationDao:
# 父单锁定后用 locking read 获取最新明细,避免 MySQL RR 继续读取确认前的旧快照。
item_result = await db.execute(
select(BizInboundItem)
.where(BizInboundItem.inbound_id == inbound_id)
.where(
BizInboundItem.inbound_id == inbound_id,
batch_scope_condition(BizInboundItem.test_batch_id, scope),
)
.order_by(BizInboundItem.pallet_id, BizInboundItem.inbound_item_id)
.with_for_update()
.execution_options(populate_existing=True)
@@ -291,6 +313,7 @@ class OperationDao:
cls,
db: AsyncSession,
shipment_id: int,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> BizShipmentOrder | None:
@@ -299,7 +322,7 @@ class OperationDao:
BizShipmentOrder,
BizShipmentOrder.shipment_id,
shipment_id,
*cls._shipment_scope_conditions(warehouse_ids, site_ids),
*cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
)
if shipment is None:
return None
@@ -307,7 +330,10 @@ class OperationDao:
# 父单锁定后读取当前明细,避免 MySQL RR 沿用确认前的快照。
item_result = await db.execute(
select(BizShipmentItem)
.where(BizShipmentItem.shipment_id == shipment_id)
.where(
BizShipmentItem.shipment_id == shipment_id,
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
)
.order_by(BizShipmentItem.pallet_id, BizShipmentItem.shipment_item_id)
.with_for_update()
.execution_options(populate_existing=True)
@@ -323,6 +349,7 @@ class OperationDao:
cls,
db: AsyncSession,
return_id: int,
scope: TestBatchScope,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> BizReturnOrder | None:
@@ -331,7 +358,7 @@ class OperationDao:
BizReturnOrder,
BizReturnOrder.return_id,
return_id,
*cls._return_scope_conditions(site_ids, warehouse_ids),
*cls._return_scope_conditions(site_ids, warehouse_ids, scope),
)
if return_order is None:
return None
@@ -339,7 +366,10 @@ class OperationDao:
# 父单锁定后使用 locking read 获取当前明细,避免 MySQL RR 沿用旧快照。
item_result = await db.execute(
select(BizReturnItem)
.where(BizReturnItem.return_id == return_id)
.where(
BizReturnItem.return_id == return_id,
batch_scope_condition(BizReturnItem.test_batch_id, scope),
)
.order_by(BizReturnItem.pallet_id, BizReturnItem.return_item_id)
.with_for_update()
.execution_options(populate_existing=True)
@@ -355,6 +385,7 @@ class OperationDao:
cls,
db: AsyncSession,
filling_id: int,
scope: TestBatchScope,
) -> BizFillingRecord | None:
"""按已锁库存的 active_filling_id 锁定仍处于活动状态的装填周期。"""
return await cls._get_for_update(
@@ -362,6 +393,7 @@ class OperationDao:
BizFillingRecord,
BizFillingRecord.filling_id,
filling_id,
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
BizFillingRecord.active_slot == 'ACTIVE',
)
@@ -370,6 +402,7 @@ class OperationDao:
cls,
db: AsyncSession,
idempotency_key: str,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None:
return await cls._get_by_key(
@@ -377,7 +410,7 @@ class OperationDao:
BizInboundOrder,
BizInboundOrder.confirm_idempotency_key,
idempotency_key,
*cls._inbound_scope_conditions(warehouse_ids),
*cls._inbound_scope_conditions(warehouse_ids, scope),
)
@classmethod
@@ -385,6 +418,7 @@ class OperationDao:
cls,
db: AsyncSession,
idempotency_key: str,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> BizShipmentOrder | None:
@@ -393,7 +427,7 @@ class OperationDao:
BizShipmentOrder,
BizShipmentOrder.confirm_idempotency_key,
idempotency_key,
*cls._shipment_scope_conditions(warehouse_ids, site_ids),
*cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
)
@classmethod
@@ -401,6 +435,7 @@ class OperationDao:
cls,
db: AsyncSession,
idempotency_key: str,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None,
) -> BizFillingRecord | None:
return await cls._get_by_key(
@@ -408,7 +443,7 @@ class OperationDao:
BizFillingRecord,
BizFillingRecord.idempotency_key,
idempotency_key,
*cls._filling_scope_conditions(warehouse_ids),
*cls._filling_scope_conditions(warehouse_ids, scope),
)
@classmethod
@@ -416,6 +451,7 @@ class OperationDao:
cls,
db: AsyncSession,
idempotency_key: str,
scope: TestBatchScope,
site_ids: list[int] | None = None,
) -> BizUsageRecord | None:
return await cls._get_by_key(
@@ -423,7 +459,7 @@ class OperationDao:
BizUsageRecord,
BizUsageRecord.idempotency_key,
idempotency_key,
*cls._usage_scope_conditions(site_ids),
*cls._usage_scope_conditions(site_ids, scope),
)
@classmethod
@@ -433,6 +469,7 @@ class OperationDao:
idempotency_key: str,
source_site_id: int,
target_warehouse_id: int,
scope: TestBatchScope,
) -> BizReturnOrder | None:
return await cls._get_by_key(
db,
@@ -441,6 +478,7 @@ class OperationDao:
idempotency_key,
BizReturnOrder.source_site_id == source_site_id,
BizReturnOrder.target_warehouse_id == target_warehouse_id,
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
)
@classmethod
@@ -450,6 +488,7 @@ class OperationDao:
idempotency_key: str,
source_site_id: int,
target_warehouse_id: int,
scope: TestBatchScope,
) -> BizReturnOrder | None:
return await cls._get_by_key(
db,
@@ -458,6 +497,7 @@ class OperationDao:
idempotency_key,
BizReturnOrder.source_site_id == source_site_id,
BizReturnOrder.target_warehouse_id == target_warehouse_id,
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
)
@classmethod
@@ -465,10 +505,11 @@ class OperationDao:
cls,
db: AsyncSession,
query_object: InboundPageQueryModel,
warehouse_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._inbound_scope_conditions(warehouse_ids)
conditions = cls._inbound_scope_conditions(warehouse_ids, scope)
if query_object.inbound_no:
conditions.append(BizInboundOrder.inbound_no.like(f'%{query_object.inbound_no}%'))
if query_object.warehouse_id:
@@ -490,6 +531,7 @@ class OperationDao:
cls,
db: AsyncSession,
inbound_id: int,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None:
return await cls._get_order_detail(
@@ -500,7 +542,8 @@ class OperationDao:
BizInboundItem,
BizInboundItem.inbound_id,
BizInboundItem.inbound_item_id,
*cls._inbound_scope_conditions(warehouse_ids),
scope,
*cls._inbound_scope_conditions(warehouse_ids, scope),
)
@classmethod
@@ -549,10 +592,11 @@ class OperationDao:
cls,
db: AsyncSession,
query_object: FillingPageQueryModel,
warehouse_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._filling_scope_conditions(warehouse_ids)
conditions = cls._filling_scope_conditions(warehouse_ids, scope)
if query_object.filling_no:
conditions.append(BizFillingRecord.filling_no.like(f'%{query_object.filling_no}%'))
if query_object.warehouse_id:
@@ -584,6 +628,7 @@ class OperationDao:
cls,
db: AsyncSession,
filling_id: int,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None,
) -> BizFillingRecord | None:
return await cls._get_by_id(
@@ -591,7 +636,7 @@ class OperationDao:
BizFillingRecord,
BizFillingRecord.filling_id,
filling_id,
*cls._filling_scope_conditions(warehouse_ids),
*cls._filling_scope_conditions(warehouse_ids, scope),
)
@classmethod
@@ -620,11 +665,12 @@ class OperationDao:
cls,
db: AsyncSession,
query_object: ShipmentPageQueryModel,
warehouse_ids: list[int] | None,
site_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._shipment_scope_conditions(warehouse_ids, site_ids)
conditions = cls._shipment_scope_conditions(warehouse_ids, site_ids, scope)
if query_object.shipment_no:
conditions.append(BizShipmentOrder.shipment_no.like(f'%{query_object.shipment_no}%'))
if query_object.source_warehouse_id:
@@ -648,6 +694,7 @@ class OperationDao:
cls,
db: AsyncSession,
shipment_id: int,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> BizShipmentOrder | None:
@@ -656,7 +703,7 @@ class OperationDao:
BizShipmentOrder,
BizShipmentOrder.shipment_id,
shipment_id,
*cls._shipment_scope_conditions(warehouse_ids, site_ids),
*cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
)
if shipment is None:
return None
@@ -674,9 +721,13 @@ class OperationDao:
BizPalletInventory.current_line_id.is_(None),
BizPalletInventory.active_filling_id.is_not(None),
BizPalletInventory.remaining_packages > 0,
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
),
)
.where(BizShipmentItem.shipment_id == shipment_id)
.where(
BizShipmentItem.shipment_id == shipment_id,
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
)
.order_by(BizShipmentItem.shipment_item_id)
)
items = []
@@ -686,7 +737,10 @@ class OperationDao:
else:
item_result = await db.execute(
select(BizShipmentItem)
.where(BizShipmentItem.shipment_id == shipment_id)
.where(
BizShipmentItem.shipment_id == shipment_id,
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
)
.order_by(BizShipmentItem.shipment_item_id)
)
items = list(item_result.scalars().all())
@@ -746,10 +800,11 @@ class OperationDao:
cls,
db: AsyncSession,
query_object: UsagePageQueryModel,
site_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True,
site_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._usage_scope_conditions(site_ids)
conditions = cls._usage_scope_conditions(site_ids, scope)
if query_object.usage_no:
conditions.append(BizUsageRecord.usage_no.like(f'%{query_object.usage_no}%'))
if query_object.site_id:
@@ -779,6 +834,7 @@ class OperationDao:
cls,
db: AsyncSession,
usage_id: int,
scope: TestBatchScope,
site_ids: list[int] | None = None,
) -> BizUsageRecord | None:
return await cls._get_by_id(
@@ -786,7 +842,7 @@ class OperationDao:
BizUsageRecord,
BizUsageRecord.usage_id,
usage_id,
*cls._usage_scope_conditions(site_ids),
*cls._usage_scope_conditions(site_ids, scope),
)
@classmethod
@@ -802,11 +858,12 @@ class OperationDao:
cls,
db: AsyncSession,
query_object: ReturnPageQueryModel,
site_ids: list[int] | None,
warehouse_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._return_scope_conditions(site_ids, warehouse_ids)
conditions = cls._return_scope_conditions(site_ids, warehouse_ids, scope)
if query_object.return_no:
conditions.append(BizReturnOrder.return_no.like(f'%{query_object.return_no}%'))
if query_object.source_site_id:
@@ -830,6 +887,7 @@ class OperationDao:
cls,
db: AsyncSession,
return_id: int,
scope: TestBatchScope,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> BizReturnOrder | None:
@@ -841,7 +899,8 @@ class OperationDao:
BizReturnItem,
BizReturnItem.return_id,
BizReturnItem.return_item_id,
*cls._return_scope_conditions(site_ids, warehouse_ids),
scope,
*cls._return_scope_conditions(site_ids, warehouse_ids, scope),
)
@classmethod
@@ -16,6 +16,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet, BizStrain
from module_business.entity.do.operation_do import BizFillingRecord, BizPalletEvent, BizPalletInventory
from module_business.entity.vo.master_vo import StrainPageQueryModel
@@ -27,6 +28,7 @@ from module_business.entity.vo.operation_vo import (
PalletScanResponseModel,
)
from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class FillingService:
@@ -180,6 +182,7 @@ class FillingService:
query_db: AsyncSession,
query_object: FillingPageQueryModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[FillingModel]:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -192,8 +195,9 @@ class FillingService:
result = await OperationDao.get_filling_list(
query_db,
query_object,
is_page,
warehouse_ids=warehouse_ids,
scope=test_scope,
is_page=is_page,
)
rows = result.rows if isinstance(result, PageModel) else result
models = [FillingModel.model_validate(record) for record in rows]
@@ -207,11 +211,13 @@ class FillingService:
query_db: AsyncSession,
filling_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> FillingModel:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
filling = await OperationDao.get_filling_by_id(
query_db,
filling_id,
test_scope,
warehouse_ids=warehouse_ids,
)
if filling is None:
@@ -223,11 +229,13 @@ class FillingService:
cls,
query_db: AsyncSession,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]:
cls._current_identity(current_user)
strains = await MasterDao.list_strain(
query_db,
StrainPageQueryModel(status='0'),
test_scope,
False,
)
options = [
@@ -247,11 +255,12 @@ class FillingService:
query_db: AsyncSession,
qr_token: str,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel:
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token)
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None:
cls._raise_pallet_not_accessible(current_user)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
if inventory.warehouse_id is None:
@@ -292,6 +301,7 @@ class FillingService:
page_object: FillingCreateModel,
request_hash: str,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -300,6 +310,7 @@ class FillingService:
recovered = await OperationDao.get_filling_by_idempotency_key(
query_db,
page_object.idempotency_key,
test_scope,
warehouse_ids=[page_object.warehouse_id],
)
if recovered is None:
@@ -321,9 +332,11 @@ class FillingService:
query_db: AsyncSession,
page_object: FillingCreateModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
recovery_transaction_ended = False
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
request_hash = cls._build_request_hash(page_object)
user_id, user_name = cls._current_identity(current_user)
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -333,6 +346,7 @@ class FillingService:
existing = await OperationDao.get_filling_by_idempotency_key(
query_db,
page_object.idempotency_key,
test_scope,
warehouse_ids=[page_object.warehouse_id],
)
if existing is not None:
@@ -356,6 +370,7 @@ class FillingService:
scanned_pallet = await InventoryDao.get_pallet_by_qr_token(
query_db,
page_object.qr_token,
test_scope,
)
if scanned_pallet is None:
cls._raise_pallet_not_accessible(current_user)
@@ -363,6 +378,7 @@ class FillingService:
inventory = await InventoryDao.get_inventory_for_update(
query_db,
scanned_pallet.pallet_id,
test_scope,
)
if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
@@ -376,6 +392,7 @@ class FillingService:
locked_pallets = await InventoryDao.get_pallets_by_ids(
query_db,
[scanned_pallet.pallet_id],
test_scope,
)
if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != scanned_pallet.pallet_id:
cls._raise_pallet_not_accessible(current_user)
@@ -385,6 +402,7 @@ class FillingService:
concurrent = await OperationDao.get_active_filling_for_update(
query_db,
inventory.active_filling_id,
test_scope,
)
if concurrent is not None and concurrent.idempotency_key == page_object.idempotency_key:
# 结束首次非锁定读取建立的 RR 快照,再按最新数据范围恢复并发成功结果。
@@ -395,11 +413,13 @@ class FillingService:
page_object,
request_hash,
current_user,
test_scope,
)
warehouse = await MasterDao.get_warehouse(
query_db,
inventory.warehouse_id,
test_scope,
for_update=True,
)
if warehouse is None:
@@ -416,6 +436,7 @@ class FillingService:
await MasterDao.get_strain(
query_db,
page_object.strain_id,
test_scope,
for_update=True,
)
)
@@ -452,6 +473,7 @@ class FillingService:
create_time=current_time,
update_by=user_name,
update_time=current_time,
test_batch_id=test_scope.batch_id,
)
filling = await OperationDao.add_filling_record(query_db, filling)
filling_id = filling.filling_id
@@ -485,6 +507,7 @@ class FillingService:
occurred_at=current_time,
idempotency_key=page_object.idempotency_key,
request_hash=request_hash,
test_batch_id=test_scope.batch_id,
)
await InventoryDao.add_event(query_db, inventory, event)
await query_db.commit()
@@ -496,6 +519,7 @@ class FillingService:
page_object,
request_hash,
current_user,
test_scope,
)
except Exception:
if not recovery_transaction_ended:
@@ -8,7 +8,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import PermissionException, ServiceException
from exceptions.exception import PermissionException, ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dao.access_dao import AccessDao
from module_business.dao.inventory_dao import InventoryDao
@@ -16,6 +16,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import (
BizInboundItem,
@@ -35,6 +36,7 @@ from module_business.entity.vo.operation_vo import (
PalletScanResponseModel,
)
from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class InboundService:
@@ -95,9 +97,10 @@ class InboundService:
query_db: AsyncSession,
warehouse_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> Any:
await AccessService.assert_warehouse_access(query_db, current_user, warehouse_id)
warehouse = await MasterDao.get_warehouse(query_db, warehouse_id)
warehouse = await MasterDao.get_warehouse(query_db, warehouse_id, test_scope)
if warehouse is None:
raise ServiceException(message='目标仓库不存在')
if warehouse.status != '0':
@@ -113,6 +116,7 @@ class InboundService:
warehouse_ids: set[int],
hidden_warehouse_ids: set[int],
require_active: bool,
test_scope: TestBatchScope,
) -> None:
"""锁定仓库及授权映射,并基于锁后最新状态重新校验写权限。"""
warehouses: dict[int, Any] = {}
@@ -121,6 +125,7 @@ class InboundService:
warehouses[warehouse_id] = await MasterDao.get_warehouse(
query_db,
warehouse_id,
test_scope,
for_update=True,
)
try:
@@ -152,6 +157,7 @@ class InboundService:
inbound: BizInboundOrder,
current_user: CurrentUserModel,
*,
test_scope: TestBatchScope,
require_active: bool = True,
) -> None:
await cls._lock_and_authorize_warehouses(
@@ -160,6 +166,7 @@ class InboundService:
warehouse_ids={inbound.warehouse_id},
hidden_warehouse_ids={inbound.warehouse_id},
require_active=require_active,
test_scope=test_scope,
)
@staticmethod
@@ -183,10 +190,11 @@ class InboundService:
cls,
query_db: AsyncSession,
pallet_ids: list[int],
test_scope: TestBatchScope,
) -> tuple[list[BizPalletInventory], list[BizPallet]]:
ordered_ids = sorted(pallet_ids)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
expected_ids = set(ordered_ids)
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
@@ -205,6 +213,7 @@ class InboundService:
pallet_ids: list[int],
user_name: str,
current_time: datetime,
test_batch_id: int | None,
) -> list[BizInboundItem]:
return [
BizInboundItem(
@@ -212,6 +221,7 @@ class InboundService:
pallet_id=pallet_id,
create_by=user_name,
create_time=current_time,
test_batch_id=test_batch_id,
)
for pallet_id in sorted(pallet_ids)
]
@@ -226,6 +236,7 @@ class InboundService:
query_db: AsyncSession,
query_object: InboundPageQueryModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[InboundModel]:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -239,8 +250,9 @@ class InboundService:
result = await OperationDao.get_inbound_list(
query_db,
query_object,
is_page,
warehouse_ids=warehouse_ids,
scope=test_scope,
is_page=is_page,
)
rows = result.rows if isinstance(result, PageModel) else result
models = [InboundModel.model_validate(record) for record in rows]
@@ -254,11 +266,13 @@ class InboundService:
query_db: AsyncSession,
inbound_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> InboundModel:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
inbound = await OperationDao.get_inbound_by_id(
query_db,
inbound_id,
test_scope,
warehouse_ids=warehouse_ids,
)
if inbound is None:
@@ -270,12 +284,14 @@ class InboundService:
cls,
query_db: AsyncSession,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
allowed_ids = None if warehouse_ids is None else set(warehouse_ids)
warehouses = await MasterDao.list_warehouse(
query_db,
WarehousePageQueryModel(status='0'),
test_scope,
False,
)
options = []
@@ -301,22 +317,23 @@ class InboundService:
qr_token: str,
query_object: InboundScanQueryModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel:
await AccessService.assert_warehouse_access(
query_db,
current_user,
query_object.warehouse_id,
)
warehouse = await MasterDao.get_warehouse(query_db, query_object.warehouse_id)
warehouse = await MasterDao.get_warehouse(query_db, query_object.warehouse_id, test_scope)
if warehouse is None:
raise ServiceException(message='目标仓库不存在')
if warehouse.status != '0':
raise ServiceException(message='目标仓库已停用')
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token)
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None:
raise ServiceException(message='托盘不存在')
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None:
raise ServiceException(message='托盘库存不存在')
cls._validate_pallet_pair(pallet, inventory)
@@ -344,22 +361,26 @@ class InboundService:
query_db: AsyncSession,
page_object: InboundCreateModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
_, user_name = cls._current_identity(current_user)
await cls._validate_active_warehouse(
query_db,
page_object.warehouse_id,
current_user,
test_scope,
)
ordered_ids = sorted(page_object.pallet_ids)
await cls._lock_and_validate_pallets(query_db, ordered_ids)
await cls._lock_and_validate_pallets(query_db, ordered_ids, test_scope)
await cls._lock_and_authorize_warehouses(
query_db,
current_user,
warehouse_ids={page_object.warehouse_id},
hidden_warehouse_ids=set(),
require_active=True,
test_scope=test_scope,
)
current_time = datetime.now()
@@ -371,12 +392,13 @@ class InboundService:
create_time=current_time,
update_by=user_name,
update_time=current_time,
test_batch_id=test_scope.batch_id,
)
inbound = await OperationDao.add_inbound(query_db, inbound)
await OperationDao.replace_inbound_items(
query_db,
inbound.inbound_id,
cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time),
cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time, test_scope.batch_id),
)
await query_db.commit()
return cls._success('新增成功')
@@ -393,13 +415,16 @@ class InboundService:
query_db: AsyncSession,
page_object: InboundUpdateModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
_, user_name = cls._current_identity(current_user)
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
inbound = await OperationDao.get_inbound_for_update(
query_db,
page_object.inbound_id,
test_scope,
warehouse_ids=warehouse_ids,
)
if inbound is None:
@@ -410,15 +435,17 @@ class InboundService:
query_db,
page_object.warehouse_id,
current_user,
test_scope,
)
ordered_ids = sorted(page_object.pallet_ids)
await cls._lock_and_validate_pallets(query_db, ordered_ids)
await cls._lock_and_validate_pallets(query_db, ordered_ids, test_scope)
await cls._lock_and_authorize_warehouses(
query_db,
current_user,
warehouse_ids={inbound.warehouse_id, page_object.warehouse_id},
hidden_warehouse_ids={inbound.warehouse_id},
require_active=True,
test_scope=test_scope,
)
current_time = datetime.now()
@@ -429,7 +456,7 @@ class InboundService:
await OperationDao.replace_inbound_items(
query_db,
inbound.inbound_id,
cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time),
cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time, test_scope.batch_id),
)
await query_db.commit()
return cls._success('修改成功')
@@ -446,47 +473,9 @@ class InboundService:
query_db: AsyncSession,
inbound_ids: list[int],
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
if not inbound_ids or any(inbound_id <= 0 for inbound_id in inbound_ids):
raise ServiceException(message='入库单ID必须为正整数')
if len(inbound_ids) != len(set(inbound_ids)):
raise ServiceException(message='入库单ID不能重复')
ordered_ids = sorted(inbound_ids)
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
locked_inbounds = []
for inbound_id in ordered_ids:
inbound = await OperationDao.get_inbound_for_update(
query_db,
inbound_id,
warehouse_ids=warehouse_ids,
)
if inbound is None:
cls._raise_inbound_not_accessible(current_user)
if inbound.status != DocumentStatus.DRAFT.value:
raise ServiceException(message='已确认入库单不允许删除')
locked_inbounds.append(inbound)
locked_warehouse_ids = {inbound.warehouse_id for inbound in locked_inbounds}
await cls._lock_and_authorize_warehouses(
query_db,
current_user,
warehouse_ids=locked_warehouse_ids,
hidden_warehouse_ids=locked_warehouse_ids,
require_active=False,
)
for inbound_id in ordered_ids:
await OperationDao.delete_inbound(query_db, inbound_id)
await query_db.commit()
return cls._success('删除成功')
except IntegrityError:
await query_db.rollback()
raise ServiceException(message='删除入库单失败,单据状态或关联数据已变化') from None
except Exception:
await query_db.rollback()
raise
raise ServiceWarning(message='不支持单条物理删除,请通过测试数据批次清理')
@staticmethod
def _is_exact_confirmed(
@@ -510,12 +499,14 @@ class InboundService:
idempotency_key: str,
request_hash: str,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
current = await OperationDao.get_inbound_by_id(
query_db,
inbound_id,
test_scope,
warehouse_ids=warehouse_ids,
)
if current is None:
@@ -523,6 +514,7 @@ class InboundService:
recovered = await OperationDao.get_inbound_by_confirm_idempotency_key(
query_db,
idempotency_key,
test_scope,
warehouse_ids=[current.warehouse_id],
)
if recovered is None:
@@ -537,6 +529,7 @@ class InboundService:
locked_recovered = await OperationDao.get_inbound_for_update(
query_db,
inbound_id,
test_scope,
warehouse_ids=warehouse_ids,
)
if locked_recovered is None:
@@ -552,6 +545,7 @@ class InboundService:
query_db,
locked_recovered,
current_user,
test_scope=test_scope,
require_active=False,
)
return cls._success('确认成功')
@@ -565,13 +559,16 @@ class InboundService:
inbound_id: int,
page_object: ConfirmCommandModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
user_id, user_name = cls._current_identity(current_user)
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
inbound = await OperationDao.get_inbound_by_id(
query_db,
inbound_id,
test_scope,
warehouse_ids=warehouse_ids,
)
if inbound is None:
@@ -582,6 +579,7 @@ class InboundService:
existing = await OperationDao.get_inbound_by_confirm_idempotency_key(
query_db,
idempotency_key,
test_scope,
warehouse_ids=[inbound.warehouse_id],
)
if existing is not None:
@@ -601,6 +599,7 @@ class InboundService:
locked_inbound = await OperationDao.get_inbound_for_update(
query_db,
inbound_id,
test_scope,
warehouse_ids=warehouse_ids,
)
if locked_inbound is None:
@@ -616,6 +615,7 @@ class InboundService:
query_db,
locked_inbound,
current_user,
test_scope=test_scope,
require_active=False,
)
await query_db.rollback()
@@ -634,11 +634,12 @@ class InboundService:
if len(pallet_ids) != len(set(pallet_ids)):
raise ServiceException(message='入库单托盘不能重复')
ordered_ids = sorted(pallet_ids)
inventories, _ = await cls._lock_and_validate_pallets(query_db, ordered_ids)
inventories, _ = await cls._lock_and_validate_pallets(query_db, ordered_ids, test_scope)
await cls._lock_and_reauthorize_warehouse(
query_db,
locked_inbound,
current_user,
test_scope=test_scope,
)
for inventory in inventories:
@@ -691,6 +692,7 @@ class InboundService:
occurred_at=current_time,
idempotency_key=idempotency_key,
request_hash=request_hash,
test_batch_id=test_scope.batch_id,
)
await InventoryDao.add_event(query_db, inventory, event)
@@ -704,6 +706,7 @@ class InboundService:
page_object.idempotency_key,
cls._build_confirm_request_hash(inbound_id),
current_user,
test_scope,
)
except Exception:
await query_db.rollback()
@@ -18,6 +18,7 @@ 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.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import (
BizFillingRecord,
@@ -37,6 +38,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
class ReturnService:
@@ -207,6 +209,7 @@ class ReturnService:
query_db: AsyncSession,
query_object: ReturnPageQueryModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[ReturnModel]:
site_ids, warehouse_ids = await cls._scopes(query_db, current_user)
@@ -224,9 +227,10 @@ class ReturnService:
result = await OperationDao.get_return_list(
query_db,
query_object,
is_page,
site_ids=site_ids,
warehouse_ids=warehouse_ids,
scope=test_scope,
is_page=is_page,
)
rows = result.rows if isinstance(result, PageModel) else result
models = [ReturnModel.model_validate(record) for record in rows]
@@ -240,11 +244,13 @@ class ReturnService:
query_db: AsyncSession,
return_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> ReturnModel:
site_ids, warehouse_ids = await cls._scopes(query_db, current_user)
return_order = await OperationDao.get_return_by_id(
query_db,
return_id,
test_scope,
site_ids=site_ids,
warehouse_ids=warehouse_ids,
)
@@ -257,11 +263,13 @@ class ReturnService:
cls,
query_db: AsyncSession,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]:
cls._current_identity(current_user)
warehouses = await MasterDao.list_warehouse(
query_db,
WarehousePageQueryModel(status='0'),
test_scope,
False,
)
options = [
@@ -333,11 +341,12 @@ class ReturnService:
query_db: AsyncSession,
qr_token: str,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel:
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token)
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None:
cls._raise_pallet_not_accessible(current_user)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None or inventory.site_id is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在或不在客户现场')
await cls._assert_pallet_site_access(query_db, current_user, inventory.site_id)
@@ -345,7 +354,7 @@ class ReturnService:
filling = None
if inventory.active_filling_id is not None:
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id)
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
if filling is None:
raise ServiceException(message='托盘活动装填周期不存在')
cls._validate_active_filling(filling, inventory)
@@ -358,12 +367,14 @@ class ReturnService:
qr_token: str,
return_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
confirm_site_ids = None if warehouse_ids is None else []
return_order = await OperationDao.get_return_by_id(
query_db,
return_id,
test_scope,
site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids,
)
@@ -372,13 +383,13 @@ class ReturnService:
if return_order.status != ReturnStatus.REQUESTED.value:
raise ServiceException(message='退回单当前状态不允许确认扫码')
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token)
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None:
cls._raise_pallet_not_accessible(current_user)
matching_items = [item for item in return_order.items if item.pallet_id == pallet.pallet_id]
if len(matching_items) != 1:
cls._raise_pallet_not_accessible(current_user, '扫码托盘不属于该退回单')
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
item = matching_items[0]
@@ -386,7 +397,7 @@ class ReturnService:
filling = None
if inventory.active_filling_id is not None:
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id)
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
if filling is None:
raise ServiceException(message='托盘活动装填周期不存在')
cls._validate_active_filling(filling, inventory)
@@ -424,12 +435,13 @@ class ReturnService:
qr_token: str,
query_object: ReturnScanQueryModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel:
normalized_qr = qr_token.strip()
if not normalized_qr:
raise ServiceException(message='托盘二维码不能为空')
if query_object.mode == 'request':
return await cls._scan_request_pallet(query_db, normalized_qr, current_user)
return await cls._scan_request_pallet(query_db, normalized_qr, current_user, test_scope)
if query_object.return_id is None:
raise ServiceException(message='确认退回扫码时必须提供退回单ID')
return await cls._scan_confirm_pallet(
@@ -437,6 +449,7 @@ class ReturnService:
normalized_qr,
query_object.return_id,
current_user,
test_scope,
)
@classmethod
@@ -447,6 +460,7 @@ class ReturnService:
source_site_id: int,
current_user: CurrentUserModel,
inventories: list[BizPalletInventory],
test_scope: TestBatchScope,
) -> tuple[
list[BizPalletInventory],
list[BizPallet],
@@ -466,7 +480,7 @@ class ReturnService:
):
raise ServiceException(message='托盘当前状态不允许发起退回')
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
if set(pallet_by_id) != set(ordered_ids):
cls._raise_pallet_not_accessible(current_user, '存在不存在或库存缺失的托盘')
@@ -481,7 +495,9 @@ class ReturnService:
}
)
for filling_id in filling_ids:
filling_by_id[filling_id] = await OperationDao.get_active_filling_for_update(query_db, filling_id)
filling_by_id[filling_id] = await OperationDao.get_active_filling_for_update(
query_db, filling_id, test_scope
)
return ordered_inventories, ordered_pallets, filling_by_id
@classmethod
@@ -513,6 +529,7 @@ class ReturnService:
inventories: list[BizPalletInventory],
user_name: str,
current_time: datetime,
test_batch_id: int | None,
) -> list[BizReturnItem]:
return [
BizReturnItem(
@@ -522,6 +539,7 @@ class ReturnService:
remaining_packages_snapshot=inventory.remaining_packages,
create_by=user_name,
create_time=current_time,
test_batch_id=test_batch_id,
)
for inventory in inventories
]
@@ -532,10 +550,12 @@ class ReturnService:
query_db: AsyncSession,
page_object: ReturnRequestModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> None:
warehouse = await MasterDao.get_warehouse(
query_db,
page_object.target_warehouse_id,
test_scope,
for_update=True,
)
if warehouse is None:
@@ -543,7 +563,7 @@ class ReturnService:
if warehouse.status != '0':
raise ServiceException(message='目标仓库已停用')
site = await MasterDao.get_site(query_db, page_object.source_site_id, for_update=True)
site = await MasterDao.get_site(query_db, page_object.source_site_id, test_scope, for_update=True)
await AccessService.assert_site_access(
query_db,
current_user,
@@ -561,9 +581,11 @@ class ReturnService:
query_db: AsyncSession,
return_order: BizReturnOrder,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> None:
site = await MasterDao.get_site(query_db, return_order.source_site_id, for_update=True)
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,
@@ -582,6 +604,7 @@ class ReturnService:
page_object: ReturnRequestModel,
request_hash: str,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
site_ids = await cls._site_scope(query_db, current_user)
@@ -593,12 +616,14 @@ class ReturnService:
page_object.idempotency_key,
source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id,
scope=test_scope,
)
if recovered is None or not cls._is_exact_request_result(recovered, page_object, request_hash):
raise ServiceException(message='申请退回失败,幂等冲突')
locked = await OperationDao.get_return_for_update(
query_db,
recovered.return_id,
test_scope,
site_ids=site_ids,
warehouse_ids=request_warehouse_ids,
)
@@ -611,7 +636,7 @@ class ReturnService:
require_items=True,
):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
await cls._lock_and_reauthorize_request_site(query_db, locked, current_user)
await cls._lock_and_reauthorize_request_site(query_db, locked, current_user, test_scope)
return cls._success('申请成功', locked.return_id)
finally:
await query_db.rollback()
@@ -622,8 +647,10 @@ class ReturnService:
query_db: AsyncSession,
page_object: ReturnRequestModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
user_id, user_name = cls._current_identity(current_user)
ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids)
request_hash = cls._build_request_hash(page_object)
@@ -644,6 +671,7 @@ class ReturnService:
page_object.idempotency_key,
source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id,
scope=test_scope,
)
if existing is not None and not cls._is_exact_request_result(existing, page_object, request_hash):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
@@ -654,6 +682,7 @@ class ReturnService:
locked_existing = await OperationDao.get_return_for_update(
query_db,
preflight_existing_id,
test_scope,
site_ids=preflight_site_ids,
warehouse_ids=request_warehouse_ids,
)
@@ -666,12 +695,12 @@ class ReturnService:
require_items=True,
):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
await cls._lock_and_reauthorize_request_site(query_db, locked_existing, current_user)
await cls._lock_and_reauthorize_request_site(query_db, locked_existing, current_user, test_scope)
return_id = locked_existing.return_id
await query_db.rollback()
return cls._success('申请成功', return_id)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
try:
await AccessService.assert_site_access(
query_db,
@@ -688,12 +717,13 @@ class ReturnService:
page_object.idempotency_key,
source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id,
scope=test_scope,
)
if fresh_existing is not None:
if not cls._is_exact_request_result(fresh_existing, page_object, request_hash):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
fresh_existing_id = int(fresh_existing.return_id)
await cls._lock_and_reauthorize_request_site(query_db, fresh_existing, current_user)
await cls._lock_and_reauthorize_request_site(query_db, fresh_existing, current_user, test_scope)
await query_db.rollback()
return cls._success('申请成功', fresh_existing_id)
@@ -703,8 +733,9 @@ class ReturnService:
page_object.source_site_id,
current_user,
inventories,
test_scope,
)
await cls._lock_new_request_route(query_db, page_object, current_user)
await cls._lock_new_request_route(query_db, page_object, current_user, test_scope)
cls._validate_locked_request_pallets(
inventories,
pallets,
@@ -732,12 +763,13 @@ class ReturnService:
create_time=current_time,
update_by=user_name,
update_time=current_time,
test_batch_id=test_scope.batch_id,
)
return_order = await OperationDao.add_return_order(query_db, return_order)
return_id = int(return_order.return_id)
await OperationDao.add_return_items(
query_db,
cls._build_return_items(return_id, inventories, user_name, current_time),
cls._build_return_items(return_id, inventories, user_name, current_time, test_scope.batch_id),
)
for inventory in inventories:
@@ -768,6 +800,7 @@ class ReturnService:
occurred_at=current_time,
idempotency_key=page_object.idempotency_key,
request_hash=request_hash,
test_batch_id=test_scope.batch_id,
)
await InventoryDao.add_event(query_db, inventory, event)
@@ -825,10 +858,11 @@ class ReturnService:
query_db: AsyncSession,
return_order: BizReturnOrder,
items: list[BizReturnItem],
test_scope: TestBatchScope,
) -> tuple[list[BizPalletInventory], dict[int, BizFillingRecord]]:
ordered_ids = sorted(item.pallet_id for item in items)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
item_by_id = {item.pallet_id: item for item in items}
@@ -857,7 +891,7 @@ class ReturnService:
}
)
for filling_id in filling_ids:
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id)
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id, test_scope)
if filling is None:
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
filling_by_id[filling_id] = filling
@@ -873,11 +907,13 @@ class ReturnService:
return_order: BizReturnOrder,
current_user: CurrentUserModel,
*,
test_scope: TestBatchScope,
require_active: bool,
) -> None:
warehouse = await MasterDao.get_warehouse(
query_db,
return_order.target_warehouse_id,
test_scope,
for_update=True,
)
try:
@@ -902,6 +938,7 @@ class ReturnService:
page_object: ReturnConfirmModel,
request_hash: str,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -909,6 +946,7 @@ class ReturnService:
route = await OperationDao.get_return_by_id(
query_db,
return_id,
test_scope,
site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids,
)
@@ -919,6 +957,7 @@ class ReturnService:
page_object.idempotency_key,
source_site_id=route.source_site_id,
target_warehouse_id=route.target_warehouse_id,
scope=test_scope,
)
if recovered is None:
raise ServiceException(message='确认退回失败,幂等冲突')
@@ -927,6 +966,7 @@ class ReturnService:
locked = await OperationDao.get_return_for_update(
query_db,
return_id,
test_scope,
site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids,
)
@@ -944,6 +984,7 @@ class ReturnService:
query_db,
locked,
current_user,
test_scope=test_scope,
require_active=False,
)
return cls._success('确认成功', return_id)
@@ -957,8 +998,10 @@ class ReturnService:
return_id: int,
page_object: ReturnConfirmModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
user_id, user_name = cls._current_identity(current_user)
scanned_ids = cls._validate_pallet_ids(page_object.scanned_pallet_ids)
request_hash = cls._build_confirm_request_hash(return_id, page_object)
@@ -967,6 +1010,7 @@ class ReturnService:
return_order = await OperationDao.get_return_by_id(
query_db,
return_id,
test_scope,
site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids,
)
@@ -985,6 +1029,7 @@ class ReturnService:
page_object.idempotency_key,
source_site_id=return_order.source_site_id,
target_warehouse_id=return_order.target_warehouse_id,
scope=test_scope,
)
if existing is not None and not cls._is_exact_confirm_result(
existing,
@@ -998,6 +1043,7 @@ class ReturnService:
locked = await OperationDao.get_return_for_update(
query_db,
return_id,
test_scope,
site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids,
)
@@ -1015,6 +1061,7 @@ class ReturnService:
query_db,
locked,
current_user,
test_scope=test_scope,
require_active=False,
)
await query_db.rollback()
@@ -1034,11 +1081,12 @@ class ReturnService:
if scanned_ids != sorted(item_ids):
raise ServiceException(message='确认扫码托盘集合与退回单明细不一致')
inventories, _ = await cls._lock_confirm_pallets(query_db, locked, items)
inventories, _ = await cls._lock_confirm_pallets(query_db, locked, items, test_scope)
await cls._lock_and_reauthorize_confirm_warehouse(
query_db,
locked,
current_user,
test_scope=test_scope,
require_active=True,
)
for inventory in inventories:
@@ -1096,6 +1144,7 @@ class ReturnService:
occurred_at=current_time,
idempotency_key=page_object.idempotency_key,
request_hash=request_hash,
test_batch_id=test_scope.batch_id,
)
await InventoryDao.add_event(query_db, inventory, event)
@@ -1109,6 +1158,7 @@ class ReturnService:
page_object,
cls._build_confirm_request_hash(return_id, page_object),
current_user,
test_scope,
)
except Exception:
await query_db.rollback()
@@ -8,7 +8,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import PermissionException, ServiceException
from exceptions.exception import PermissionException, ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dao.access_dao import AccessDao
from module_business.dao.inventory_dao import InventoryDao
@@ -16,6 +16,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import (
BizFillingRecord,
@@ -36,6 +37,7 @@ from module_business.entity.vo.operation_vo import (
ShipmentUpdateModel,
)
from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class ShipmentService:
@@ -97,15 +99,16 @@ class ShipmentService:
source_warehouse_id: int,
target_site_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> tuple[Any, Any]:
await AccessService.assert_warehouse_access(query_db, current_user, source_warehouse_id)
warehouse = await MasterDao.get_warehouse(query_db, source_warehouse_id)
warehouse = await MasterDao.get_warehouse(query_db, source_warehouse_id, test_scope)
if warehouse is None:
raise ServiceException(message='来源仓库不存在')
if warehouse.status != '0':
raise ServiceException(message='来源仓库已停用')
await AccessService.assert_site_access(query_db, current_user, target_site_id)
site = await MasterDao.get_site(query_db, target_site_id)
site = await MasterDao.get_site(query_db, target_site_id, test_scope)
if site is None:
raise ServiceException(message='目标现场不存在')
if site.status != '0':
@@ -158,10 +161,11 @@ class ShipmentService:
query_db: AsyncSession,
pallet_ids: list[int],
source_warehouse_id: int,
test_scope: TestBatchScope,
) -> tuple[list[BizPalletInventory], list[BizPallet], list[BizFillingRecord]]:
ordered_ids = cls._validate_pallet_ids(pallet_ids)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
if (
len(inventories) != len(ordered_ids)
or len(pallets) != len(ordered_ids)
@@ -181,7 +185,7 @@ class ShipmentService:
fillings = []
for filling_id in sorted(set(active_filling_ids)):
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id)
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id, test_scope)
if filling is None:
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
fillings.append(filling)
@@ -207,6 +211,7 @@ class ShipmentService:
pallet_ids: list[int],
user_name: str,
current_time: datetime,
test_batch_id: int | None,
) -> list[BizShipmentItem]:
return [
BizShipmentItem(
@@ -216,6 +221,7 @@ class ShipmentService:
package_qty=None,
create_by=user_name,
create_time=current_time,
test_batch_id=test_batch_id,
)
for pallet_id in sorted(pallet_ids)
]
@@ -239,6 +245,7 @@ class ShipmentService:
query_db: AsyncSession,
query_object: ShipmentPageQueryModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[ShipmentModel]:
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
@@ -268,9 +275,10 @@ class ShipmentService:
result = await OperationDao.get_shipment_list(
query_db,
query_object,
is_page,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
scope=test_scope,
is_page=is_page,
)
rows = result.rows if isinstance(result, PageModel) else result
models = [ShipmentModel.model_validate(record) for record in rows]
@@ -284,11 +292,13 @@ class ShipmentService:
query_db: AsyncSession,
shipment_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> ShipmentModel:
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
shipment = await OperationDao.get_shipment_by_id(
query_db,
shipment_id,
test_scope,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
@@ -301,12 +311,14 @@ class ShipmentService:
cls,
query_db: AsyncSession,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]:
warehouse_ids, _ = await cls._scopes(query_db, current_user)
allowed_ids = None if warehouse_ids is None else set(warehouse_ids)
warehouses = await MasterDao.list_warehouse(
query_db,
WarehousePageQueryModel(status='0'),
test_scope,
False,
)
options = []
@@ -330,12 +342,14 @@ class ShipmentService:
cls,
query_db: AsyncSession,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]:
_, site_ids = await cls._scopes(query_db, current_user)
allowed_ids = None if site_ids is None else set(site_ids)
sites = await MasterDao.list_site(
query_db,
SitePageQueryModel(status='0'),
test_scope,
False,
)
options = []
@@ -388,13 +402,14 @@ class ShipmentService:
qr_token: str,
query_object: ShipmentScanQueryModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel:
await AccessService.assert_warehouse_access(
query_db,
current_user,
query_object.source_warehouse_id,
)
warehouse = await MasterDao.get_warehouse(query_db, query_object.source_warehouse_id)
warehouse = await MasterDao.get_warehouse(query_db, query_object.source_warehouse_id, test_scope)
if warehouse is None:
raise ServiceException(message='来源仓库不存在')
if warehouse.status != '0':
@@ -404,16 +419,16 @@ class ShipmentService:
current_user,
query_object.target_site_id,
)
site = await MasterDao.get_site(query_db, query_object.target_site_id)
site = await MasterDao.get_site(query_db, query_object.target_site_id, test_scope)
if site is None:
raise ServiceException(message='目标现场不存在')
if site.status != '0':
raise ServiceException(message='目标现场已停用')
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token)
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None:
cls._raise_pallet_not_accessible(current_user)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
if inventory.pallet_id != pallet.pallet_id or inventory.warehouse_id != query_object.source_warehouse_id:
@@ -422,7 +437,7 @@ class ShipmentService:
cls._raise_pallet_not_accessible(current_user, '托盘已在客户现场')
if inventory.active_filling_id is None:
raise ServiceException(message='托盘不存在活动装填周期')
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id)
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
if filling is None:
raise ServiceException(message='托盘活动装填周期不存在')
cls._validate_scan_pallet(
@@ -455,20 +470,24 @@ class ShipmentService:
query_db: AsyncSession,
page_object: ShipmentCreateModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
_, user_name = cls._current_identity(current_user)
await cls._validate_active_route(
query_db,
page_object.source_warehouse_id,
page_object.target_site_id,
current_user,
test_scope,
)
ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids)
await cls._lock_and_validate_pallets(
query_db,
ordered_ids,
page_object.source_warehouse_id,
test_scope,
)
await cls._lock_and_authorize_routes(
query_db,
@@ -478,6 +497,7 @@ class ShipmentService:
hidden_warehouse_ids=set(),
hidden_site_ids=set(),
require_active=True,
test_scope=test_scope,
)
current_time = datetime.now()
@@ -490,12 +510,13 @@ class ShipmentService:
create_time=current_time,
update_by=user_name,
update_time=current_time,
test_batch_id=test_scope.batch_id,
)
shipment = await OperationDao.add_shipment(query_db, shipment)
await OperationDao.replace_shipment_items(
query_db,
shipment.shipment_id,
cls._build_items(shipment.shipment_id, ordered_ids, user_name, current_time),
cls._build_items(shipment.shipment_id, ordered_ids, user_name, current_time, test_scope.batch_id),
)
await query_db.commit()
return cls._success('新增成功')
@@ -512,13 +533,16 @@ class ShipmentService:
query_db: AsyncSession,
page_object: ShipmentUpdateModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
_, user_name = cls._current_identity(current_user)
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
shipment = await OperationDao.get_shipment_for_update(
query_db,
page_object.shipment_id,
test_scope,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
@@ -534,12 +558,14 @@ class ShipmentService:
page_object.source_warehouse_id,
page_object.target_site_id,
current_user,
test_scope,
)
ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids)
await cls._lock_and_validate_pallets(
query_db,
ordered_ids,
page_object.source_warehouse_id,
test_scope,
)
await cls._lock_and_authorize_routes(
query_db,
@@ -555,6 +581,7 @@ class ShipmentService:
hidden_warehouse_ids={shipment.source_warehouse_id},
hidden_site_ids={shipment.target_site_id},
require_active=True,
test_scope=test_scope,
)
current_time = datetime.now()
@@ -566,7 +593,7 @@ class ShipmentService:
await OperationDao.replace_shipment_items(
query_db,
shipment.shipment_id,
cls._build_items(shipment.shipment_id, ordered_ids, user_name, current_time),
cls._build_items(shipment.shipment_id, ordered_ids, user_name, current_time, test_scope.batch_id),
)
await query_db.commit()
return cls._success('修改成功')
@@ -583,53 +610,9 @@ class ShipmentService:
query_db: AsyncSession,
shipment_ids: list[int],
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
if not shipment_ids or any(
type(shipment_id) is not int or shipment_id <= 0 for shipment_id in shipment_ids
):
raise ServiceException(message='发货单ID必须为正整数')
if len(shipment_ids) != len(set(shipment_ids)):
raise ServiceException(message='发货单ID不能重复')
ordered_ids = sorted(shipment_ids)
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
locked_shipments = []
for shipment_id in ordered_ids:
shipment = await OperationDao.get_shipment_for_update(
query_db,
shipment_id,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
if shipment is None:
cls._raise_shipment_not_accessible(current_user)
if shipment.status != DocumentStatus.DRAFT.value:
raise ServiceException(message='已确认发货单不允许删除')
locked_shipments.append(shipment)
route_warehouse_ids = {shipment.source_warehouse_id for shipment in locked_shipments}
route_site_ids = {shipment.target_site_id for shipment in locked_shipments}
await cls._lock_and_authorize_routes(
query_db,
current_user,
warehouse_ids=route_warehouse_ids,
site_ids=route_site_ids,
hidden_warehouse_ids=route_warehouse_ids,
hidden_site_ids=route_site_ids,
require_active=False,
)
for shipment_id in ordered_ids:
await OperationDao.delete_shipment(query_db, shipment_id)
await query_db.commit()
return cls._success('删除成功')
except IntegrityError:
await query_db.rollback()
raise ServiceException(message='删除发货单失败,单据状态或关联数据已变化') from None
except Exception:
await query_db.rollback()
raise
raise ServiceWarning(message='不支持单条物理删除,请通过测试数据批次清理')
@staticmethod
def _is_exact_confirmed(
@@ -656,6 +639,7 @@ class ShipmentService:
hidden_warehouse_ids: set[int],
hidden_site_ids: set[int],
require_active: bool,
test_scope: TestBatchScope,
) -> None:
warehouses: dict[int, Any] = {}
warehouse_permission_errors: dict[int, PermissionException] = {}
@@ -663,6 +647,7 @@ class ShipmentService:
warehouse = await MasterDao.get_warehouse(
query_db,
warehouse_id,
test_scope,
for_update=True,
)
warehouses[warehouse_id] = warehouse
@@ -682,6 +667,7 @@ class ShipmentService:
site = await MasterDao.get_site(
query_db,
site_id,
test_scope,
for_update=True,
)
sites[site_id] = site
@@ -727,6 +713,7 @@ class ShipmentService:
shipment: BizShipmentOrder,
current_user: CurrentUserModel,
*,
test_scope: TestBatchScope,
require_active: bool = True,
) -> None:
await cls._lock_and_authorize_routes(
@@ -737,6 +724,7 @@ class ShipmentService:
hidden_warehouse_ids={shipment.source_warehouse_id},
hidden_site_ids={shipment.target_site_id},
require_active=require_active,
test_scope=test_scope,
)
@classmethod
@@ -747,12 +735,14 @@ class ShipmentService:
idempotency_key: str,
request_hash: str,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
current = await OperationDao.get_shipment_by_id(
query_db,
shipment_id,
test_scope,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
@@ -761,6 +751,7 @@ class ShipmentService:
recovered = await OperationDao.get_shipment_by_confirm_idempotency_key(
query_db,
idempotency_key,
test_scope,
warehouse_ids=[current.source_warehouse_id],
site_ids=[current.target_site_id],
)
@@ -776,6 +767,7 @@ class ShipmentService:
locked_recovered = await OperationDao.get_shipment_for_update(
query_db,
shipment_id,
test_scope,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
@@ -792,6 +784,7 @@ class ShipmentService:
query_db,
locked_recovered,
current_user,
test_scope=test_scope,
require_active=False,
)
return cls._success('确认成功')
@@ -805,13 +798,16 @@ class ShipmentService:
shipment_id: int,
page_object: ConfirmCommandModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
user_id, user_name = cls._current_identity(current_user)
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
shipment = await OperationDao.get_shipment_by_id(
query_db,
shipment_id,
test_scope,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
@@ -823,6 +819,7 @@ class ShipmentService:
existing = await OperationDao.get_shipment_by_confirm_idempotency_key(
query_db,
idempotency_key,
test_scope,
warehouse_ids=[shipment.source_warehouse_id],
site_ids=[shipment.target_site_id],
)
@@ -837,6 +834,7 @@ class ShipmentService:
locked_shipment = await OperationDao.get_shipment_for_update(
query_db,
shipment_id,
test_scope,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
@@ -853,6 +851,7 @@ class ShipmentService:
query_db,
locked_shipment,
current_user,
test_scope=test_scope,
require_active=False,
)
await query_db.rollback()
@@ -874,8 +873,9 @@ class ShipmentService:
query_db,
ordered_ids,
locked_shipment.source_warehouse_id,
test_scope,
)
await cls._lock_and_reauthorize_route(query_db, locked_shipment, current_user)
await cls._lock_and_reauthorize_route(query_db, locked_shipment, current_user, test_scope=test_scope)
for inventory in inventories:
PalletStateMachine.ensure_transition(
@@ -930,6 +930,7 @@ class ShipmentService:
occurred_at=current_time,
idempotency_key=idempotency_key,
request_hash=request_hash,
test_batch_id=test_scope.batch_id,
)
await InventoryDao.add_event(query_db, inventory, event)
@@ -943,6 +944,7 @@ class ShipmentService:
page_object.idempotency_key,
cls._build_confirm_request_hash(shipment_id),
current_user,
test_scope,
)
except Exception:
await query_db.rollback()
@@ -18,6 +18,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.operation_do import BizPalletEvent, BizUsageRecord
from module_business.entity.vo.master_vo import LinePageQueryModel
from module_business.entity.vo.operation_vo import (
@@ -29,6 +30,7 @@ from module_business.entity.vo.operation_vo import (
UsageSuccessResponseModel,
)
from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class UsageService:
@@ -147,8 +149,9 @@ class UsageService:
query_db: AsyncSession,
site_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> None:
site = await MasterDao.get_site(query_db, site_id, for_update=True)
site = await MasterDao.get_site(query_db, site_id, test_scope, for_update=True)
try:
await AccessService.assert_site_access(
query_db,
@@ -171,6 +174,7 @@ class UsageService:
request_hash: str,
current_user: CurrentUserModel,
expected_site_id: int,
test_scope: TestBatchScope,
) -> CrudResponseModel:
try:
site_ids = await cls._site_scope(query_db, current_user)
@@ -179,6 +183,7 @@ class UsageService:
recovered = await OperationDao.get_usage_by_idempotency_key(
query_db,
page_object.idempotency_key,
test_scope,
site_ids=[expected_site_id],
)
if recovered is None:
@@ -191,6 +196,7 @@ class UsageService:
query_db,
recovered.site_id,
current_user,
test_scope,
)
return cls._success(recovered)
finally:
@@ -202,6 +208,7 @@ class UsageService:
query_db: AsyncSession,
query_object: UsagePageQueryModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[UsageModel]:
site_ids = await cls._site_scope(query_db, current_user)
@@ -220,8 +227,9 @@ class UsageService:
result = await OperationDao.get_usage_list(
query_db,
query_object,
is_page,
site_ids=site_ids,
scope=test_scope,
is_page=is_page,
)
rows = result.rows if isinstance(result, PageModel) else result
models = [UsageModel.model_validate(record) for record in rows]
@@ -235,11 +243,13 @@ class UsageService:
query_db: AsyncSession,
usage_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> UsageModel:
site_ids = await cls._site_scope(query_db, current_user)
usage = await OperationDao.get_usage_by_id(
query_db,
usage_id,
test_scope,
site_ids=site_ids,
)
if usage is None:
@@ -252,10 +262,11 @@ class UsageService:
query_db: AsyncSession,
site_id: int,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]:
cls._current_identity(current_user)
await AccessService.assert_site_access(query_db, current_user, site_id)
site = await MasterDao.get_site(query_db, site_id)
site = await MasterDao.get_site(query_db, site_id, test_scope)
if site is None:
raise ServiceException(message='客户现场不存在')
if site.status != '0':
@@ -263,6 +274,7 @@ class UsageService:
lines = await MasterDao.list_line(
query_db,
LinePageQueryModel(siteId=site_id, status='0'),
test_scope,
False,
)
options = [
@@ -282,12 +294,13 @@ class UsageService:
query_db: AsyncSession,
qr_token: str,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel:
normalized_qr = qr_token.strip()
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, normalized_qr)
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, normalized_qr, test_scope)
if pallet is None:
cls._raise_pallet_not_accessible(current_user)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
if inventory.site_id is None:
@@ -311,7 +324,7 @@ class UsageService:
or inventory.remaining_packages > pallet.capacity_packages
):
raise ServiceException(message='托盘当前状态不允许使用')
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id)
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
if filling is None:
raise ServiceException(message='托盘活动装填周期不存在')
if (
@@ -345,14 +358,16 @@ class UsageService:
query_db: AsyncSession,
page_object: UsageCreateModel,
current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel:
expected_site_id: int | None = None
try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
request_hash = cls._build_request_hash(page_object)
user_id, user_name = cls._current_identity(current_user)
preflight_site_ids = await cls._site_scope(query_db, current_user)
# 产线归属是请求内稳定路由;锁事务仍以库存中的实际现场复核。
preflight_line = await MasterDao.get_line(query_db, page_object.line_id)
preflight_line = await MasterDao.get_line(query_db, page_object.line_id, test_scope)
preflight_site_id = preflight_line.site_id if preflight_line is not None else None
existing = None
if preflight_site_id is not None and (
@@ -361,6 +376,7 @@ class UsageService:
existing = await OperationDao.get_usage_by_idempotency_key(
query_db,
page_object.idempotency_key,
test_scope,
site_ids=[preflight_site_id],
)
if existing is not None:
@@ -372,6 +388,7 @@ class UsageService:
query_db,
existing.site_id,
current_user,
test_scope,
)
result = cls._success(existing)
await query_db.rollback()
@@ -380,6 +397,7 @@ class UsageService:
scanned_pallet = await InventoryDao.get_pallet_by_qr_token(
query_db,
page_object.qr_token.strip(),
test_scope,
)
if scanned_pallet is None:
cls._raise_pallet_not_accessible(current_user)
@@ -389,6 +407,7 @@ class UsageService:
inventory = await InventoryDao.get_inventory_for_update(
query_db,
scanned_pallet_id,
test_scope,
)
if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
@@ -403,6 +422,7 @@ class UsageService:
locked_pallets = await InventoryDao.get_pallets_by_ids(
query_db,
[inventory.pallet_id],
test_scope,
)
if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != inventory.pallet_id:
cls._raise_pallet_not_accessible(current_user)
@@ -412,6 +432,7 @@ class UsageService:
await OperationDao.get_active_filling_for_update(
query_db,
inventory.active_filling_id,
test_scope,
)
if inventory.active_filling_id is not None
else None
@@ -419,6 +440,7 @@ class UsageService:
locked_existing = await OperationDao.get_usage_by_idempotency_key(
query_db,
page_object.idempotency_key,
test_scope,
site_ids=[inventory.site_id],
)
if locked_existing is not None:
@@ -432,6 +454,7 @@ class UsageService:
query_db,
locked_existing.site_id,
current_user,
test_scope,
)
result = cls._success(locked_existing)
await query_db.rollback()
@@ -442,7 +465,7 @@ class UsageService:
if filling is None:
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
site = await MasterDao.get_site(query_db, inventory.site_id, for_update=True)
site = await MasterDao.get_site(query_db, inventory.site_id, test_scope, for_update=True)
await cls._assert_pallet_site_access(
query_db,
current_user,
@@ -453,7 +476,7 @@ class UsageService:
raise ServiceException(message='客户现场不存在')
if site.status != '0':
raise ServiceException(message='客户现场已停用')
line = await MasterDao.get_line(query_db, page_object.line_id, for_update=True)
line = await MasterDao.get_line(query_db, page_object.line_id, test_scope, for_update=True)
if line is None:
raise ServiceException(message='产线不存在')
if line.status != '0':
@@ -512,6 +535,7 @@ class UsageService:
request_hash=request_hash,
operator_id=user_id,
occurred_at=current_time,
test_batch_id=test_scope.batch_id,
)
usage = await OperationDao.add_usage_record(query_db, usage)
usage_id = usage.usage_id
@@ -553,6 +577,7 @@ class UsageService:
occurred_at=current_time,
idempotency_key=page_object.idempotency_key,
request_hash=request_hash,
test_batch_id=test_scope.batch_id,
)
await InventoryDao.add_event(query_db, inventory, event)
await query_db.commit()
@@ -567,6 +592,7 @@ class UsageService:
request_hash,
current_user,
expected_site_id,
test_scope,
)
except Exception:
await query_db.rollback()