安全:隔离主数据并封闭单条删除

This commit is contained in:
焦龙言
2026-09-22 18:16:30 +08:00
parent af5428d0e3
commit b6f606d9f2
13 changed files with 1382 additions and 1131 deletions
@@ -12,6 +12,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.master_vo import CustomerModel, CustomerPageQueryModel, DeleteCustomerModel
from module_business.service.master_service import MasterService
from utils.response_util import ResponseUtil
@@ -32,8 +34,9 @@ async def get_master_customer_list(
request: Request,
customer_page_query: Annotated[CustomerPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
customer_page_query_result = await MasterService.list_customer(query_db, customer_page_query, is_page=True)
customer_page_query_result = await MasterService.list_customer(query_db, customer_page_query, scope, is_page=True)
return ResponseUtil.success(model_content=customer_page_query_result)
@@ -49,8 +52,9 @@ async def query_detail_master_customer(
request: Request,
customer_id: Annotated[int, Path(gt=0, description='客户ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
customer_detail_result = await MasterService.detail_customer(query_db, customer_id)
customer_detail_result = await MasterService.detail_customer(query_db, customer_id, scope)
return ResponseUtil.success(data=customer_detail_result)
@@ -68,13 +72,14 @@ async def add_master_customer(
add_customer: CustomerModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
current_time = datetime.now()
add_customer.create_by = current_user.user.user_name
add_customer.create_time = current_time
add_customer.update_by = current_user.user.user_name
add_customer.update_time = current_time
add_customer_result = await MasterService.add_customer(query_db, add_customer)
add_customer_result = await MasterService.add_customer(query_db, add_customer, current_user, scope)
return ResponseUtil.success(msg=add_customer_result.message)
@@ -92,10 +97,11 @@ async def edit_master_customer(
edit_customer: CustomerModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
edit_customer.update_by = current_user.user.user_name
edit_customer.update_time = datetime.now()
edit_customer_result = await MasterService.edit_customer(query_db, edit_customer)
edit_customer_result = await MasterService.edit_customer(query_db, edit_customer, current_user, scope)
return ResponseUtil.success(msg=edit_customer_result.message)
@@ -12,6 +12,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.master_vo import DeleteLineModel, LineModel, LinePageQueryModel
from module_business.service.master_service import MasterService
from utils.response_util import ResponseUtil
@@ -30,8 +32,9 @@ async def get_master_line_list(
request: Request,
line_page_query: Annotated[LinePageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
line_page_query_result = await MasterService.list_line(query_db, line_page_query, is_page=True)
line_page_query_result = await MasterService.list_line(query_db, line_page_query, scope, is_page=True)
return ResponseUtil.success(model_content=line_page_query_result)
@@ -47,8 +50,9 @@ async def query_detail_master_line(
request: Request,
line_id: Annotated[int, Path(gt=0, description='生产线ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
line_detail_result = await MasterService.detail_line(query_db, line_id)
line_detail_result = await MasterService.detail_line(query_db, line_id, scope)
return ResponseUtil.success(data=line_detail_result)
@@ -66,13 +70,14 @@ async def add_master_line(
add_line: LineModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
current_time = datetime.now()
add_line.create_by = current_user.user.user_name
add_line.create_time = current_time
add_line.update_by = current_user.user.user_name
add_line.update_time = current_time
add_line_result = await MasterService.add_line(query_db, add_line)
add_line_result = await MasterService.add_line(query_db, add_line, current_user, scope)
return ResponseUtil.success(msg=add_line_result.message)
@@ -90,10 +95,11 @@ async def edit_master_line(
edit_line: LineModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
edit_line.update_by = current_user.user.user_name
edit_line.update_time = datetime.now()
edit_line_result = await MasterService.edit_line(query_db, edit_line)
edit_line_result = await MasterService.edit_line(query_db, edit_line, current_user, scope)
return ResponseUtil.success(msg=edit_line_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.master_vo import (
DeletePalletModel,
PalletCreateModel,
@@ -37,8 +39,9 @@ async def get_master_pallet_list(
request: Request,
pallet_page_query: Annotated[PalletPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await PalletService.list_pallet(query_db, pallet_page_query, is_page=True)
result = await PalletService.list_pallet(query_db, pallet_page_query, scope, is_page=True)
return ResponseUtil.success(model_content=result)
@@ -54,8 +57,9 @@ async def query_master_pallet_qrcode(
request: Request,
pallet_id: Annotated[int, Path(gt=0, description='托盘ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
png = await PalletService.get_pallet_qrcode(query_db, pallet_id)
png = await PalletService.get_pallet_qrcode(query_db, pallet_id, scope)
return Response(content=png, media_type='image/png')
@@ -70,8 +74,9 @@ async def query_detail_master_pallet(
request: Request,
pallet_id: Annotated[int, Path(gt=0, description='托盘ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
result = await PalletService.detail_pallet(query_db, pallet_id)
result = await PalletService.detail_pallet(query_db, pallet_id, scope)
return ResponseUtil.success(data=result)
@@ -88,8 +93,9 @@ async def add_master_pallet(
add_pallet: PalletCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await PalletService.add_pallet(query_db, add_pallet, current_user.user.user_name)
result = await PalletService.add_pallet(query_db, add_pallet, current_user, scope)
return ResponseUtil.success(msg=result.message)
@@ -106,8 +112,9 @@ async def edit_master_pallet(
edit_pallet: PalletEditModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await PalletService.edit_pallet(query_db, edit_pallet, current_user.user.user_name)
result = await PalletService.edit_pallet(query_db, edit_pallet, current_user, scope)
return ResponseUtil.success(msg=result.message)
@@ -12,6 +12,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.master_vo import (
BusinessUserOptionModel,
DeleteSiteModel,
@@ -45,8 +47,9 @@ async def get_master_site_users(
request: Request,
site_id: Annotated[int, Path(gt=0, description='客户现场ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
users = await PalletService.list_site_users(query_db, site_id)
users = await PalletService.list_site_users(query_db, site_id, scope)
return ResponseUtil.success(data=users)
@@ -64,8 +67,9 @@ async def assign_master_site_users(
assignment: UserAssignModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, ASSIGN_CURRENT_USER()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await PalletService.assign_site_users(query_db, site_id, assignment)
result = await PalletService.assign_site_users(query_db, site_id, assignment, current_user, scope)
return ResponseUtil.success(msg=result.message)
@@ -80,8 +84,9 @@ async def get_master_site_list(
request: Request,
site_page_query: Annotated[SitePageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
site_page_query_result = await MasterService.list_site(query_db, site_page_query, is_page=True)
site_page_query_result = await MasterService.list_site(query_db, site_page_query, scope, is_page=True)
return ResponseUtil.success(model_content=site_page_query_result)
@@ -97,8 +102,9 @@ async def query_detail_master_site(
request: Request,
site_id: Annotated[int, Path(gt=0, description='客户现场ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
site_detail_result = await MasterService.detail_site(query_db, site_id)
site_detail_result = await MasterService.detail_site(query_db, site_id, scope)
return ResponseUtil.success(data=site_detail_result)
@@ -116,13 +122,14 @@ async def add_master_site(
add_site: SiteModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
current_time = datetime.now()
add_site.create_by = current_user.user.user_name
add_site.create_time = current_time
add_site.update_by = current_user.user.user_name
add_site.update_time = current_time
add_site_result = await MasterService.add_site(query_db, add_site)
add_site_result = await MasterService.add_site(query_db, add_site, current_user, scope)
return ResponseUtil.success(msg=add_site_result.message)
@@ -140,10 +147,11 @@ async def edit_master_site(
edit_site: SiteModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
edit_site.update_by = current_user.user.user_name
edit_site.update_time = datetime.now()
edit_site_result = await MasterService.edit_site(query_db, edit_site)
edit_site_result = await MasterService.edit_site(query_db, edit_site, current_user, scope)
return ResponseUtil.success(msg=edit_site_result.message)
@@ -12,6 +12,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.master_vo import DeleteStrainModel, StrainModel, StrainPageQueryModel
from module_business.service.master_service import MasterService
from utils.response_util import ResponseUtil
@@ -32,8 +34,9 @@ async def get_master_strain_list(
request: Request,
strain_page_query: Annotated[StrainPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
strain_page_query_result = await MasterService.list_strain(query_db, strain_page_query, is_page=True)
strain_page_query_result = await MasterService.list_strain(query_db, strain_page_query, scope, is_page=True)
return ResponseUtil.success(model_content=strain_page_query_result)
@@ -49,8 +52,9 @@ async def query_detail_master_strain(
request: Request,
strain_id: Annotated[int, Path(gt=0, description='菌种ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
strain_detail_result = await MasterService.detail_strain(query_db, strain_id)
strain_detail_result = await MasterService.detail_strain(query_db, strain_id, scope)
return ResponseUtil.success(data=strain_detail_result)
@@ -68,13 +72,14 @@ async def add_master_strain(
add_strain: StrainModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
current_time = datetime.now()
add_strain.create_by = current_user.user.user_name
add_strain.create_time = current_time
add_strain.update_by = current_user.user.user_name
add_strain.update_time = current_time
add_strain_result = await MasterService.add_strain(query_db, add_strain)
add_strain_result = await MasterService.add_strain(query_db, add_strain, current_user, scope)
return ResponseUtil.success(msg=add_strain_result.message)
@@ -92,10 +97,11 @@ async def edit_master_strain(
edit_strain: StrainModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
edit_strain.update_by = current_user.user.user_name
edit_strain.update_time = datetime.now()
edit_strain_result = await MasterService.edit_strain(query_db, edit_strain)
edit_strain_result = await MasterService.edit_strain(query_db, edit_strain, current_user, scope)
return ResponseUtil.success(msg=edit_strain_result.message)
@@ -12,6 +12,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.master_vo import (
BusinessUserOptionModel,
DeleteWarehouseModel,
@@ -45,8 +47,9 @@ async def get_master_warehouse_users(
request: Request,
warehouse_id: Annotated[int, Path(gt=0, description='仓库ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
users = await PalletService.list_warehouse_users(query_db, warehouse_id)
users = await PalletService.list_warehouse_users(query_db, warehouse_id, scope)
return ResponseUtil.success(data=users)
@@ -64,8 +67,9 @@ async def assign_master_warehouse_users(
assignment: UserAssignModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, ASSIGN_CURRENT_USER()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
result = await PalletService.assign_warehouse_users(query_db, warehouse_id, assignment)
result = await PalletService.assign_warehouse_users(query_db, warehouse_id, assignment, current_user, scope)
return ResponseUtil.success(msg=result.message)
@@ -80,8 +84,14 @@ async def get_master_warehouse_list(
request: Request,
warehouse_page_query: Annotated[WarehousePageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
warehouse_page_query_result = await MasterService.list_warehouse(query_db, warehouse_page_query, is_page=True)
warehouse_page_query_result = await MasterService.list_warehouse(
query_db,
warehouse_page_query,
scope,
is_page=True,
)
return ResponseUtil.success(model_content=warehouse_page_query_result)
@@ -97,8 +107,9 @@ async def query_detail_master_warehouse(
request: Request,
warehouse_id: Annotated[int, Path(gt=0, description='仓库ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response:
warehouse_detail_result = await MasterService.detail_warehouse(query_db, warehouse_id)
warehouse_detail_result = await MasterService.detail_warehouse(query_db, warehouse_id, scope)
return ResponseUtil.success(data=warehouse_detail_result)
@@ -116,13 +127,14 @@ async def add_master_warehouse(
add_warehouse: WarehouseModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
current_time = datetime.now()
add_warehouse.create_by = current_user.user.user_name
add_warehouse.create_time = current_time
add_warehouse.update_by = current_user.user.user_name
add_warehouse.update_time = current_time
add_warehouse_result = await MasterService.add_warehouse(query_db, add_warehouse)
add_warehouse_result = await MasterService.add_warehouse(query_db, add_warehouse, current_user, scope)
return ResponseUtil.success(msg=add_warehouse_result.message)
@@ -140,10 +152,11 @@ async def edit_master_warehouse(
edit_warehouse: WarehouseModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response:
edit_warehouse.update_by = current_user.user.user_name
edit_warehouse.update_time = datetime.now()
edit_warehouse_result = await MasterService.edit_warehouse(query_db, edit_warehouse)
edit_warehouse_result = await MasterService.edit_warehouse(query_db, edit_warehouse, current_user, scope)
return ResponseUtil.success(msg=edit_warehouse_result.message)
@@ -1,14 +1,14 @@
from datetime import datetime
from typing import Any, Literal
from typing import Any
from pydantic import BaseModel
from sqlalchemy import ColumnElement, Select, and_, delete, func, or_, select, update
from sqlalchemy import ColumnElement, Select, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel
from module_admin.entity.do.user_do import SysUser
from module_business.domain.enums import PalletFlowState
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
from module_business.entity.do.master_do import (
BizCustomer,
BizCustomerSite,
@@ -19,18 +19,7 @@ from module_business.entity.do.master_do import (
BizUserWarehouse,
BizWarehouse,
)
from module_business.entity.do.operation_do import (
BizFillingRecord,
BizInboundItem,
BizInboundOrder,
BizPalletEvent,
BizPalletInventory,
BizReturnItem,
BizReturnOrder,
BizShipmentItem,
BizShipmentOrder,
BizUsageRecord,
)
from module_business.entity.do.operation_do import BizPalletInventory
from module_business.entity.vo.master_vo import (
CustomerModel,
CustomerPageQueryModel,
@@ -47,54 +36,6 @@ from module_business.entity.vo.master_vo import (
)
from utils.page_util import PageUtil
MasterResource = Literal['strain', 'customer', 'site', 'line', 'warehouse']
_REFERENCE_COLUMNS: dict[MasterResource, tuple[tuple[InstrumentedAttribute, ...], ...]] = {
'strain': ((BizFillingRecord.strain_id,),),
'customer': ((BizCustomerSite.customer_id,),),
'site': (
(BizProductionLine.site_id,),
(BizUserSite.site_id,),
(BizShipmentOrder.target_site_id,),
(BizUsageRecord.site_id,),
(BizReturnOrder.source_site_id,),
(BizPalletInventory.site_id,),
(BizPalletEvent.before_site_id, BizPalletEvent.after_site_id),
),
'line': (
(BizUsageRecord.line_id,),
(BizPalletInventory.current_line_id,),
(BizPalletEvent.before_line_id, BizPalletEvent.after_line_id),
),
'warehouse': (
(BizUserWarehouse.warehouse_id,),
(BizInboundOrder.warehouse_id,),
(BizFillingRecord.warehouse_id,),
(BizShipmentOrder.source_warehouse_id,),
(BizReturnOrder.target_warehouse_id,),
(BizPalletInventory.warehouse_id,),
(BizPalletEvent.before_warehouse_id, BizPalletEvent.after_warehouse_id),
),
}
_MASTER_ID_COLUMNS: dict[MasterResource, InstrumentedAttribute] = {
'strain': BizStrain.strain_id,
'customer': BizCustomer.customer_id,
'site': BizCustomerSite.site_id,
'line': BizProductionLine.line_id,
'warehouse': BizWarehouse.warehouse_id,
}
_PALLET_REMOVAL_BLOCKING_COLUMNS: tuple[InstrumentedAttribute, ...] = (
BizInboundItem.pallet_id,
BizFillingRecord.pallet_id,
BizShipmentItem.pallet_id,
BizUsageRecord.pallet_id,
BizReturnItem.pallet_id,
BizPalletEvent.pallet_id,
)
class MasterDao:
"""菌种、客户、现场、产线和仓库主数据数据库操作层。"""
@@ -120,10 +61,11 @@ class MasterDao:
model: type[Any],
id_column: InstrumentedAttribute,
resource_id: int,
scope_predicate: ColumnElement[bool],
*,
for_update: bool = False,
) -> Any | None:
statement = select(model).where(id_column == resource_id)
statement = select(model).where(id_column == resource_id, scope_predicate)
if for_update:
statement = statement.with_for_update().execution_options(populate_existing=True)
result = await db.execute(statement)
@@ -146,8 +88,10 @@ class MasterDao:
model: type[Any],
id_field: str,
page_object: BaseModel,
test_batch_id: int | None,
) -> Any:
payload = page_object.model_dump(exclude={id_field, 'can_remove'})
payload = page_object.model_dump(exclude={id_field, 'can_remove', 'test_batch_id'})
payload['test_batch_id'] = test_batch_id
record = model(**payload)
db.add(record)
await db.flush()
@@ -160,63 +104,31 @@ class MasterDao:
id_column: InstrumentedAttribute,
id_field: str,
page_object: BaseModel,
scope: TestBatchScope,
) -> None:
payload = page_object.model_dump(
exclude_unset=True,
exclude={'can_remove', 'create_by', 'create_time'},
exclude={'can_remove', 'create_by', 'create_time', 'test_batch_id'},
)
resource_id = payload.pop(id_field)
await db.execute(update(model).where(id_column == resource_id).values(**payload))
@staticmethod
async def _delete(
db: AsyncSession,
model: type[Any],
id_column: InstrumentedAttribute,
resource_id: int,
) -> None:
await db.execute(delete(model).where(id_column == resource_id))
@classmethod
async def get_master_removability(
cls,
db: AsyncSession,
resource_type: MasterResource,
resource_ids: list[int],
) -> dict[int, bool]:
"""按删除约束批量判断五类主数据是否允许删除。"""
removability = dict.fromkeys(resource_ids, False)
if not resource_ids:
return removability
id_column = _MASTER_ID_COLUMNS.get(resource_type)
references = _REFERENCE_COLUMNS.get(resource_type)
if id_column is None or references is None:
raise ValueError(f'不支持的主数据类型:{resource_type}')
has_no_references = []
for columns in references:
predicates = [column == id_column for column in columns]
predicate = predicates[0] if len(predicates) == 1 else or_(*predicates)
has_no_references.append(~select(columns[0]).where(predicate).exists())
statement = select(
id_column,
and_(*has_no_references).label('can_remove'),
).where(id_column.in_(resource_ids))
result = await db.execute(statement)
for resource_id, can_remove in result.all():
removability[resource_id] = bool(can_remove)
return removability
await db.execute(
update(model)
.where(
id_column == resource_id,
batch_scope_condition(model.test_batch_id, scope),
)
.values(**payload)
)
@classmethod
async def list_strain(
cls,
db: AsyncSession,
query_object: StrainPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
conditions = [batch_scope_condition(BizStrain.test_batch_id, scope)]
if query_object.strain_code:
conditions.append(BizStrain.strain_code.like(f'%{query_object.strain_code}%'))
if query_object.strain_name:
@@ -231,6 +143,7 @@ class MasterDao:
cls,
db: AsyncSession,
strain_id: int,
scope: TestBatchScope,
*,
for_update: bool = False,
) -> BizStrain | None:
@@ -239,6 +152,7 @@ class MasterDao:
BizStrain,
BizStrain.strain_id,
strain_id,
batch_scope_condition(BizStrain.test_batch_id, scope),
for_update=for_update,
)
@@ -247,25 +161,22 @@ class MasterDao:
return await cls._find(db, BizStrain, BizStrain.strain_code, strain_code)
@classmethod
async def add_strain(cls, db: AsyncSession, strain: StrainModel) -> BizStrain:
return await cls._add(db, BizStrain, 'strain_id', strain)
async def add_strain(cls, db: AsyncSession, strain: StrainModel, test_batch_id: int | None) -> BizStrain:
return await cls._add(db, BizStrain, 'strain_id', strain, test_batch_id)
@classmethod
async def update_strain(cls, db: AsyncSession, strain: StrainModel) -> None:
await cls._update(db, BizStrain, BizStrain.strain_id, 'strain_id', strain)
@classmethod
async def delete_strain(cls, db: AsyncSession, strain_id: int) -> None:
await cls._delete(db, BizStrain, BizStrain.strain_id, strain_id)
async def update_strain(cls, db: AsyncSession, strain: StrainModel, scope: TestBatchScope) -> None:
await cls._update(db, BizStrain, BizStrain.strain_id, 'strain_id', strain, scope)
@classmethod
async def list_customer(
cls,
db: AsyncSession,
query_object: CustomerPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
conditions = [batch_scope_condition(BizCustomer.test_batch_id, scope)]
if query_object.customer_code:
conditions.append(BizCustomer.customer_code.like(f'%{query_object.customer_code}%'))
if query_object.customer_name:
@@ -276,33 +187,44 @@ class MasterDao:
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_customer(cls, db: AsyncSession, customer_id: int) -> BizCustomer | None:
return await cls._get(db, BizCustomer, BizCustomer.customer_id, customer_id)
async def get_customer(
cls,
db: AsyncSession,
customer_id: int,
scope: TestBatchScope,
*,
for_update: bool = False,
) -> BizCustomer | None:
return await cls._get(
db,
BizCustomer,
BizCustomer.customer_id,
customer_id,
batch_scope_condition(BizCustomer.test_batch_id, scope),
for_update=for_update,
)
@classmethod
async def find_customer_by_code(cls, db: AsyncSession, customer_code: str) -> BizCustomer | None:
return await cls._find(db, BizCustomer, BizCustomer.customer_code, customer_code)
@classmethod
async def add_customer(cls, db: AsyncSession, customer: CustomerModel) -> BizCustomer:
return await cls._add(db, BizCustomer, 'customer_id', customer)
async def add_customer(cls, db: AsyncSession, customer: CustomerModel, test_batch_id: int | None) -> BizCustomer:
return await cls._add(db, BizCustomer, 'customer_id', customer, test_batch_id)
@classmethod
async def update_customer(cls, db: AsyncSession, customer: CustomerModel) -> None:
await cls._update(db, BizCustomer, BizCustomer.customer_id, 'customer_id', customer)
@classmethod
async def delete_customer(cls, db: AsyncSession, customer_id: int) -> None:
await cls._delete(db, BizCustomer, BizCustomer.customer_id, customer_id)
async def update_customer(cls, db: AsyncSession, customer: CustomerModel, scope: TestBatchScope) -> None:
await cls._update(db, BizCustomer, BizCustomer.customer_id, 'customer_id', customer, scope)
@classmethod
async def list_site(
cls,
db: AsyncSession,
query_object: SitePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
conditions = [batch_scope_condition(BizCustomerSite.test_batch_id, scope)]
if query_object.customer_id:
conditions.append(BizCustomerSite.customer_id == query_object.customer_id)
if query_object.site_code:
@@ -319,6 +241,7 @@ class MasterDao:
cls,
db: AsyncSession,
site_id: int,
scope: TestBatchScope,
*,
for_update: bool = False,
) -> BizCustomerSite | None:
@@ -327,6 +250,7 @@ class MasterDao:
BizCustomerSite,
BizCustomerSite.site_id,
site_id,
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
for_update=for_update,
)
@@ -346,25 +270,22 @@ class MasterDao:
)
@classmethod
async def add_site(cls, db: AsyncSession, site: SiteModel) -> BizCustomerSite:
return await cls._add(db, BizCustomerSite, 'site_id', site)
async def add_site(cls, db: AsyncSession, site: SiteModel, test_batch_id: int | None) -> BizCustomerSite:
return await cls._add(db, BizCustomerSite, 'site_id', site, test_batch_id)
@classmethod
async def update_site(cls, db: AsyncSession, site: SiteModel) -> None:
await cls._update(db, BizCustomerSite, BizCustomerSite.site_id, 'site_id', site)
@classmethod
async def delete_site(cls, db: AsyncSession, site_id: int) -> None:
await cls._delete(db, BizCustomerSite, BizCustomerSite.site_id, site_id)
async def update_site(cls, db: AsyncSession, site: SiteModel, scope: TestBatchScope) -> None:
await cls._update(db, BizCustomerSite, BizCustomerSite.site_id, 'site_id', site, scope)
@classmethod
async def list_line(
cls,
db: AsyncSession,
query_object: LinePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
conditions = [batch_scope_condition(BizProductionLine.test_batch_id, scope)]
if query_object.site_id:
conditions.append(BizProductionLine.site_id == query_object.site_id)
if query_object.line_code:
@@ -381,6 +302,7 @@ class MasterDao:
cls,
db: AsyncSession,
line_id: int,
scope: TestBatchScope,
*,
for_update: bool = False,
) -> BizProductionLine | None:
@@ -389,6 +311,7 @@ class MasterDao:
BizProductionLine,
BizProductionLine.line_id,
line_id,
batch_scope_condition(BizProductionLine.test_batch_id, scope),
for_update=for_update,
)
@@ -408,25 +331,22 @@ class MasterDao:
)
@classmethod
async def add_line(cls, db: AsyncSession, line: LineModel) -> BizProductionLine:
return await cls._add(db, BizProductionLine, 'line_id', line)
async def add_line(cls, db: AsyncSession, line: LineModel, test_batch_id: int | None) -> BizProductionLine:
return await cls._add(db, BizProductionLine, 'line_id', line, test_batch_id)
@classmethod
async def update_line(cls, db: AsyncSession, line: LineModel) -> None:
await cls._update(db, BizProductionLine, BizProductionLine.line_id, 'line_id', line)
@classmethod
async def delete_line(cls, db: AsyncSession, line_id: int) -> None:
await cls._delete(db, BizProductionLine, BizProductionLine.line_id, line_id)
async def update_line(cls, db: AsyncSession, line: LineModel, scope: TestBatchScope) -> None:
await cls._update(db, BizProductionLine, BizProductionLine.line_id, 'line_id', line, scope)
@classmethod
async def list_warehouse(
cls,
db: AsyncSession,
query_object: WarehousePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
conditions = [batch_scope_condition(BizWarehouse.test_batch_id, scope)]
if query_object.warehouse_code:
conditions.append(BizWarehouse.warehouse_code.like(f'%{query_object.warehouse_code}%'))
if query_object.warehouse_name:
@@ -441,6 +361,7 @@ class MasterDao:
cls,
db: AsyncSession,
warehouse_id: int,
scope: TestBatchScope,
*,
for_update: bool = False,
) -> BizWarehouse | None:
@@ -449,6 +370,7 @@ class MasterDao:
BizWarehouse,
BizWarehouse.warehouse_id,
warehouse_id,
batch_scope_condition(BizWarehouse.test_batch_id, scope),
for_update=for_update,
)
@@ -457,25 +379,22 @@ class MasterDao:
return await cls._find(db, BizWarehouse, BizWarehouse.warehouse_code, warehouse_code)
@classmethod
async def add_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel) -> BizWarehouse:
return await cls._add(db, BizWarehouse, 'warehouse_id', warehouse)
async def add_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel, test_batch_id: int | None) -> BizWarehouse:
return await cls._add(db, BizWarehouse, 'warehouse_id', warehouse, test_batch_id)
@classmethod
async def update_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel) -> None:
await cls._update(db, BizWarehouse, BizWarehouse.warehouse_id, 'warehouse_id', warehouse)
@classmethod
async def delete_warehouse(cls, db: AsyncSession, warehouse_id: int) -> None:
await cls._delete(db, BizWarehouse, BizWarehouse.warehouse_id, warehouse_id)
async def update_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel, scope: TestBatchScope) -> None:
await cls._update(db, BizWarehouse, BizWarehouse.warehouse_id, 'warehouse_id', warehouse, scope)
@classmethod
async def list_pallet(
cls,
db: AsyncSession,
query_object: PalletPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
conditions = [batch_scope_condition(BizPallet.test_batch_id, scope)]
if query_object.pallet_code:
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
if query_object.asset_status:
@@ -488,45 +407,18 @@ class MasterDao:
cls,
db: AsyncSession,
pallet_id: int,
scope: TestBatchScope,
*,
for_update: bool = False,
) -> BizPallet | None:
statement = select(BizPallet).where(BizPallet.pallet_id == pallet_id)
if for_update:
statement = statement.with_for_update()
result = await db.execute(statement)
return result.scalar_one_or_none()
@classmethod
async def get_pallet_removability(
cls,
db: AsyncSession,
pallet_ids: list[int],
) -> dict[int, bool]:
"""批量判断托盘是否仍为未入库且没有任何业务引用。"""
removability = dict.fromkeys(pallet_ids, False)
if not pallet_ids:
return removability
has_unstocked_inventory = (
select(BizPalletInventory.pallet_id)
.where(
BizPalletInventory.pallet_id == BizPallet.pallet_id,
BizPalletInventory.flow_state == PalletFlowState.UNSTOCKED.value,
)
.exists()
)
has_no_business_references = tuple(
~select(column).where(column == BizPallet.pallet_id).exists() for column in _PALLET_REMOVAL_BLOCKING_COLUMNS
)
statement = select(
return await cls._get(
db,
BizPallet,
BizPallet.pallet_id,
and_(has_unstocked_inventory, *has_no_business_references).label('can_remove'),
).where(BizPallet.pallet_id.in_(pallet_ids))
result = await db.execute(statement)
for pallet_id, can_remove in result.all():
removability[pallet_id] = bool(can_remove)
return removability
pallet_id,
batch_scope_condition(BizPallet.test_batch_id, scope),
for_update=for_update,
)
@classmethod
async def find_pallet_by_code(cls, db: AsyncSession, pallet_code: str) -> BizPallet | None:
@@ -543,12 +435,16 @@ class MasterDao:
cls,
db: AsyncSession,
pallet: PalletEditModel,
scope: TestBatchScope,
update_by: str,
update_time: datetime,
) -> None:
await db.execute(
update(BizPallet)
.where(BizPallet.pallet_id == pallet.pallet_id)
.where(
BizPallet.pallet_id == pallet.pallet_id,
batch_scope_condition(BizPallet.test_batch_id, scope),
)
.values(
capacity_packages=pallet.capacity_packages,
asset_status=pallet.asset_status,
@@ -558,21 +454,21 @@ class MasterDao:
)
)
@classmethod
async def delete_pallet(cls, db: AsyncSession, pallet_id: int) -> None:
await cls._delete(db, BizPallet, BizPallet.pallet_id, pallet_id)
@classmethod
async def get_pallet_inventory(
cls,
db: AsyncSession,
pallet_id: int,
scope: TestBatchScope,
*,
for_update: bool = False,
) -> BizPalletInventory | None:
statement = select(BizPalletInventory).where(BizPalletInventory.pallet_id == pallet_id)
statement = select(BizPalletInventory).where(
BizPalletInventory.pallet_id == pallet_id,
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
)
if for_update:
statement = statement.with_for_update()
statement = statement.with_for_update().execution_options(populate_existing=True)
result = await db.execute(statement)
return result.scalar_one_or_none()
@@ -586,17 +482,6 @@ class MasterDao:
await db.flush()
return inventory
@classmethod
async def delete_pallet_inventory(cls, db: AsyncSession, pallet_id: int) -> None:
await cls._delete(db, BizPalletInventory, BizPalletInventory.pallet_id, pallet_id)
@classmethod
async def count_pallet_events(cls, db: AsyncSession, pallet_id: int) -> int:
result = await db.execute(
select(func.count('*')).select_from(BizPalletEvent).where(BizPalletEvent.pallet_id == pallet_id)
)
return result.scalar() or 0
@classmethod
async def list_assignable_users(cls, db: AsyncSession) -> list[Any]:
result = await db.execute(
@@ -622,31 +507,35 @@ class MasterDao:
return list(result.all())
@classmethod
async def list_warehouse_assigned_user_ids(cls, db: AsyncSession, warehouse_id: int) -> list[int]:
result = await db.execute(select(BizUserWarehouse.user_id).where(BizUserWarehouse.warehouse_id == warehouse_id))
return list(result.scalars().all())
@classmethod
async def list_site_assigned_user_ids(cls, db: AsyncSession, site_id: int) -> list[int]:
result = await db.execute(select(BizUserSite.user_id).where(BizUserSite.site_id == site_id))
return list(result.scalars().all())
@classmethod
async def count_references(
async def list_warehouse_assigned_user_ids(
cls,
db: AsyncSession,
resource_type: MasterResource,
resource_id: int,
) -> int:
"""统计主数据的所有直接外键引用;同表的多个位置字段按一行计数。"""
references = _REFERENCE_COLUMNS.get(resource_type)
if references is None:
raise ValueError(f'不支持的主数据类型:{resource_type}')
warehouse_id: int,
scope: TestBatchScope,
) -> list[int]:
result = await db.execute(
select(BizUserWarehouse.user_id)
.join(BizWarehouse, BizWarehouse.warehouse_id == BizUserWarehouse.warehouse_id)
.where(
BizUserWarehouse.warehouse_id == warehouse_id,
batch_scope_condition(BizWarehouse.test_batch_id, scope),
)
)
return list(result.scalars().all())
total = 0
for columns in references:
predicates = [column == resource_id for column in columns]
predicate = predicates[0] if len(predicates) == 1 else or_(*predicates)
result = await db.execute(select(func.count('*')).select_from(columns[0].class_).where(predicate))
total += result.scalar() or 0
return total
@classmethod
async def list_site_assigned_user_ids(
cls,
db: AsyncSession,
site_id: int,
scope: TestBatchScope,
) -> list[int]:
result = await db.execute(
select(BizUserSite.user_id)
.join(BizCustomerSite, BizCustomerSite.site_id == BizUserSite.site_id)
.where(
BizUserSite.site_id == site_id,
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
)
)
return list(result.scalars().all())
@@ -32,11 +32,6 @@ class StrainModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class StrainPageQueryModel(BaseModel):
@@ -91,11 +86,6 @@ class CustomerModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class CustomerPageQueryModel(BaseModel):
@@ -158,11 +148,6 @@ class SiteModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class SitePageQueryModel(BaseModel):
@@ -219,11 +204,6 @@ class LineModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class LinePageQueryModel(BaseModel):
@@ -280,11 +260,6 @@ class WarehouseModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class WarehousePageQueryModel(BaseModel):
@@ -341,7 +316,6 @@ class PalletModel(BaseModel):
create_time: datetime | None = Field(default=None, description='创建时间')
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
can_remove: bool | None = Field(default=None, description='是否允许删除')
class PalletCreateModel(BaseModel):
@@ -1,4 +1,4 @@
from typing import Any, Literal
from typing import Any, Literal, NoReturn
from pydantic import BaseModel
from sqlalchemy.exc import IntegrityError
@@ -6,7 +6,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dao.master_dao import MasterDao
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
from module_business.entity.vo.master_vo import (
CustomerModel,
CustomerPageQueryModel,
@@ -24,6 +26,7 @@ from module_business.entity.vo.master_vo import (
WarehouseModel,
WarehousePageQueryModel,
)
from module_business.service.test_batch_access_service import TestBatchAccessService
MasterResource = Literal['strain', 'customer', 'site', 'line', 'warehouse']
@@ -71,17 +74,35 @@ class MasterService:
"""菌种、客户、现场、产线和仓库主数据服务层。"""
@staticmethod
async def _validate_parent(db: AsyncSession, resource: MasterResource, page_object: BaseModel) -> None:
async def _prepare_test_write(
db: AsyncSession,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> None:
if scope.mode != 'TEST':
return
TestBatchAccessService.assert_super_admin(current_user)
await TestBatchAccessService.lock_active_batch(db, scope)
@staticmethod
async def _validate_parent(
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
scope: TestBatchScope,
) -> None:
if resource == 'site':
customer = await MasterDao.get_customer(db, page_object.customer_id)
customer = await MasterDao.get_customer(db, page_object.customer_id, scope)
if customer is None:
raise ServiceException(message='目标客户不存在')
assert_record_in_scope(customer, scope, '目标客户')
if customer.status != '0':
raise ServiceException(message='目标客户已停用')
elif resource == 'line':
site = await MasterDao.get_site(db, page_object.site_id)
site = await MasterDao.get_site(db, page_object.site_id, scope)
if site is None:
raise ServiceException(message='目标客户现场不存在')
assert_record_in_scope(site, scope, '目标客户现场')
if site.status != '0':
raise ServiceException(message='目标客户现场已停用')
@@ -127,50 +148,37 @@ class MasterService:
code = getattr(page_object, config['code_field'])
raise ServiceException(message=f'{config["label"]}编码{code}已存在')
@staticmethod
def _extract_ids(resource: MasterResource, value: int | BaseModel) -> list[int]:
ids = [value] if isinstance(value, int) else list(getattr(value, _RESOURCE_CONFIG[resource]['ids_field']))
if not ids or any(resource_id <= 0 for resource_id in ids):
raise ServiceException(message='主数据ID必须为正整数')
if len(ids) != len(set(ids)):
raise ServiceException(message='主数据ID不能重复')
return ids
@classmethod
async def _list(
cls,
db: AsyncSession,
resource: MasterResource,
query_object: BaseModel,
scope: TestBatchScope,
is_page: bool,
) -> PageModel | list[BaseModel]:
result = await getattr(MasterDao, f'list_{resource}')(db, query_object, is_page)
result = await getattr(MasterDao, f'list_{resource}')(db, query_object, scope, is_page)
config = _RESOURCE_CONFIG[resource]
records = result.rows if isinstance(result, PageModel) else result
models = [config['vo'].model_validate(record) for record in records]
id_field = config['id_field']
removability = await MasterDao.get_master_removability(
db,
resource,
[getattr(model, id_field) for model in models],
)
rows = [
model.model_copy(update={'can_remove': removability.get(getattr(model, id_field), False)})
for model in models
]
rows = [config['vo'].model_validate(record) for record in records]
if isinstance(result, PageModel):
return result.model_copy(update={'rows': rows})
return rows
@classmethod
async def _detail(cls, db: AsyncSession, resource: MasterResource, resource_id: int) -> BaseModel:
async def _detail(
cls,
db: AsyncSession,
resource: MasterResource,
resource_id: int,
scope: TestBatchScope,
) -> BaseModel:
config = _RESOURCE_CONFIG[resource]
record = await getattr(MasterDao, f'get_{resource}')(db, resource_id)
record = await getattr(MasterDao, f'get_{resource}')(db, resource_id, scope)
if record is None:
raise ServiceException(message=f'{config["label"]}不存在')
model = config['vo'].model_validate(record)
removability = await MasterDao.get_master_removability(db, resource, [resource_id])
return model.model_copy(update={'can_remove': removability.get(resource_id, False)})
assert_record_in_scope(record, scope, config['label'])
return config['vo'].model_validate(record)
@classmethod
async def _add(
@@ -178,13 +186,16 @@ class MasterService:
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
config = _RESOURCE_CONFIG[resource]
try:
await cls._validate_parent(db, resource, page_object)
await cls._prepare_test_write(db, current_user, scope)
await cls._validate_parent(db, resource, page_object, scope)
await cls._validate_unique(db, resource, page_object)
safe_object = page_object.model_copy(update={config['id_field']: None})
await getattr(MasterDao, f'add_{resource}')(db, safe_object)
await getattr(MasterDao, f'add_{resource}')(db, safe_object, scope.batch_id)
await db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except IntegrityError:
@@ -200,18 +211,27 @@ class MasterService:
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
config = _RESOURCE_CONFIG[resource]
try:
await cls._prepare_test_write(db, current_user, scope)
resource_id = getattr(page_object, config['id_field'])
if resource_id is None:
raise ServiceException(message=f'{config["label"]}ID不能为空')
current = await getattr(MasterDao, f'get_{resource}')(db, resource_id)
current = await getattr(MasterDao, f'get_{resource}')(
db,
resource_id,
scope,
for_update=scope.mode == 'TEST',
)
if current is None:
raise ServiceException(message=f'{config["label"]}不存在')
await cls._validate_parent(db, resource, page_object)
assert_record_in_scope(current, scope, config['label'])
await cls._validate_parent(db, resource, page_object, scope)
await cls._validate_unique(db, resource, page_object, resource_id)
await getattr(MasterDao, f'update_{resource}')(db, page_object)
await getattr(MasterDao, f'update_{resource}')(db, page_object, scope)
await db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except IntegrityError:
@@ -221,173 +241,216 @@ class MasterService:
await db.rollback()
raise
@classmethod
async def _remove(
cls,
db: AsyncSession,
resource: MasterResource,
value: int | BaseModel,
) -> CrudResponseModel:
config = _RESOURCE_CONFIG[resource]
try:
resource_ids = cls._extract_ids(resource, value)
for resource_id in resource_ids:
if await MasterDao.count_references(db, resource, resource_id) > 0:
raise ServiceWarning(message=f'{config["label"]}已被引用,只能停用,不能删除')
if await getattr(MasterDao, f'get_{resource}')(db, resource_id) is None:
raise ServiceException(message=f'{config["label"]}不存在')
for resource_id in resource_ids:
await getattr(MasterDao, f'delete_{resource}')(db, resource_id)
await db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except IntegrityError:
await db.rollback()
raise ServiceException(message=f'删除{config["label"]}失败,该数据仍被引用') from None
except Exception:
await db.rollback()
raise
@staticmethod
async def reject_physical_delete() -> NoReturn:
raise ServiceWarning('', '不支持单条物理删除,请通过测试数据批次清理')
@classmethod
async def list_strain(
cls,
db: AsyncSession,
query_object: StrainPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[StrainModel]:
return await cls._list(db, 'strain', query_object, is_page)
return await cls._list(db, 'strain', query_object, scope, is_page)
@classmethod
async def detail_strain(cls, db: AsyncSession, strain_id: int) -> StrainModel:
return await cls._detail(db, 'strain', strain_id)
async def detail_strain(cls, db: AsyncSession, strain_id: int, scope: TestBatchScope) -> StrainModel:
return await cls._detail(db, 'strain', strain_id, scope)
@classmethod
async def add_strain(cls, db: AsyncSession, strain: StrainModel) -> CrudResponseModel:
return await cls._add(db, 'strain', strain)
async def add_strain(
cls,
db: AsyncSession,
strain: StrainModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'strain', strain, current_user, scope)
@classmethod
async def edit_strain(cls, db: AsyncSession, strain: StrainModel) -> CrudResponseModel:
return await cls._edit(db, 'strain', strain)
async def edit_strain(
cls,
db: AsyncSession,
strain: StrainModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'strain', strain, current_user, scope)
@classmethod
async def remove_strain(
cls,
db: AsyncSession,
value: int | DeleteStrainModel,
) -> CrudResponseModel:
return await cls._remove(db, 'strain', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@classmethod
async def list_customer(
cls,
db: AsyncSession,
query_object: CustomerPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[CustomerModel]:
return await cls._list(db, 'customer', query_object, is_page)
return await cls._list(db, 'customer', query_object, scope, is_page)
@classmethod
async def detail_customer(cls, db: AsyncSession, customer_id: int) -> CustomerModel:
return await cls._detail(db, 'customer', customer_id)
async def detail_customer(cls, db: AsyncSession, customer_id: int, scope: TestBatchScope) -> CustomerModel:
return await cls._detail(db, 'customer', customer_id, scope)
@classmethod
async def add_customer(cls, db: AsyncSession, customer: CustomerModel) -> CrudResponseModel:
return await cls._add(db, 'customer', customer)
async def add_customer(
cls,
db: AsyncSession,
customer: CustomerModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'customer', customer, current_user, scope)
@classmethod
async def edit_customer(cls, db: AsyncSession, customer: CustomerModel) -> CrudResponseModel:
return await cls._edit(db, 'customer', customer)
async def edit_customer(
cls,
db: AsyncSession,
customer: CustomerModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'customer', customer, current_user, scope)
@classmethod
async def remove_customer(
cls,
db: AsyncSession,
value: int | DeleteCustomerModel,
) -> CrudResponseModel:
return await cls._remove(db, 'customer', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@classmethod
async def list_site(
cls,
db: AsyncSession,
query_object: SitePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[SiteModel]:
return await cls._list(db, 'site', query_object, is_page)
return await cls._list(db, 'site', query_object, scope, is_page)
@classmethod
async def detail_site(cls, db: AsyncSession, site_id: int) -> SiteModel:
return await cls._detail(db, 'site', site_id)
async def detail_site(cls, db: AsyncSession, site_id: int, scope: TestBatchScope) -> SiteModel:
return await cls._detail(db, 'site', site_id, scope)
@classmethod
async def add_site(cls, db: AsyncSession, site: SiteModel) -> CrudResponseModel:
return await cls._add(db, 'site', site)
async def add_site(
cls,
db: AsyncSession,
site: SiteModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'site', site, current_user, scope)
@classmethod
async def edit_site(cls, db: AsyncSession, site: SiteModel) -> CrudResponseModel:
return await cls._edit(db, 'site', site)
async def edit_site(
cls,
db: AsyncSession,
site: SiteModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'site', site, current_user, scope)
@classmethod
async def remove_site(
cls,
db: AsyncSession,
value: int | DeleteSiteModel,
) -> CrudResponseModel:
return await cls._remove(db, 'site', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@classmethod
async def list_line(
cls,
db: AsyncSession,
query_object: LinePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[LineModel]:
return await cls._list(db, 'line', query_object, is_page)
return await cls._list(db, 'line', query_object, scope, is_page)
@classmethod
async def detail_line(cls, db: AsyncSession, line_id: int) -> LineModel:
return await cls._detail(db, 'line', line_id)
async def detail_line(cls, db: AsyncSession, line_id: int, scope: TestBatchScope) -> LineModel:
return await cls._detail(db, 'line', line_id, scope)
@classmethod
async def add_line(cls, db: AsyncSession, line: LineModel) -> CrudResponseModel:
return await cls._add(db, 'line', line)
async def add_line(
cls,
db: AsyncSession,
line: LineModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'line', line, current_user, scope)
@classmethod
async def edit_line(cls, db: AsyncSession, line: LineModel) -> CrudResponseModel:
return await cls._edit(db, 'line', line)
async def edit_line(
cls,
db: AsyncSession,
line: LineModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'line', line, current_user, scope)
@classmethod
async def remove_line(
cls,
db: AsyncSession,
value: int | DeleteLineModel,
) -> CrudResponseModel:
return await cls._remove(db, 'line', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@classmethod
async def list_warehouse(
cls,
db: AsyncSession,
query_object: WarehousePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[WarehouseModel]:
return await cls._list(db, 'warehouse', query_object, is_page)
return await cls._list(db, 'warehouse', query_object, scope, is_page)
@classmethod
async def detail_warehouse(cls, db: AsyncSession, warehouse_id: int) -> WarehouseModel:
return await cls._detail(db, 'warehouse', warehouse_id)
async def detail_warehouse(cls, db: AsyncSession, warehouse_id: int, scope: TestBatchScope) -> WarehouseModel:
return await cls._detail(db, 'warehouse', warehouse_id, scope)
@classmethod
async def add_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel) -> CrudResponseModel:
return await cls._add(db, 'warehouse', warehouse)
async def add_warehouse(
cls,
db: AsyncSession,
warehouse: WarehouseModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'warehouse', warehouse, current_user, scope)
@classmethod
async def edit_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel) -> CrudResponseModel:
return await cls._edit(db, 'warehouse', warehouse)
async def edit_warehouse(
cls,
db: AsyncSession,
warehouse: WarehouseModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'warehouse', warehouse, current_user, scope)
@classmethod
async def remove_warehouse(
cls,
db: AsyncSession,
value: int | DeleteWarehouseModel,
) -> CrudResponseModel:
return await cls._remove(db, 'warehouse', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@@ -1,16 +1,18 @@
import secrets
from datetime import datetime
from io import BytesIO
from typing import Literal
from typing import Literal, NoReturn
import qrcode
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from exceptions.exception import ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dao.access_dao import AccessDao
from module_business.dao.master_dao import MasterDao
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import BizPalletInventory
from module_business.entity.vo.master_vo import (
@@ -22,6 +24,7 @@ from module_business.entity.vo.master_vo import (
PalletPageQueryModel,
UserAssignModel,
)
from module_business.service.test_batch_access_service import TestBatchAccessService
AssignmentResource = Literal['warehouse', 'site']
@@ -41,33 +44,43 @@ class PalletService:
qrcode.make(qr_token).save(buffer, format='PNG')
return buffer.getvalue()
@staticmethod
async def _prepare_test_write(
db: AsyncSession,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> None:
if scope.mode != 'TEST':
return
TestBatchAccessService.assert_super_admin(current_user)
await TestBatchAccessService.lock_active_batch(db, scope)
@classmethod
async def get_pallet_qrcode(cls, db: AsyncSession, pallet_id: int) -> bytes:
pallet = await MasterDao.get_pallet(db, pallet_id)
async def get_pallet_qrcode(cls, db: AsyncSession, pallet_id: int, scope: TestBatchScope) -> bytes:
pallet = await MasterDao.get_pallet(db, pallet_id, scope)
if pallet is None:
raise ServiceException(message='托盘不存在')
assert_record_in_scope(pallet, scope, '托盘')
return cls.render_qr_png(pallet.qr_token)
@staticmethod
def _extract_pallet_ids(value: int | DeletePalletModel) -> list[int]:
pallet_ids = [value] if isinstance(value, int) else list(value.pallet_ids)
if not pallet_ids or any(pallet_id <= 0 for pallet_id in pallet_ids):
raise ServiceException(message='托盘ID必须为正整数')
if len(pallet_ids) != len(set(pallet_ids)):
raise ServiceException(message='托盘ID不能重复')
return sorted(pallet_ids)
@classmethod
async def _lock_pallet_inventory(cls, db: AsyncSession, pallet_id: int) -> tuple[BizPallet, BizPalletInventory]:
async def _lock_pallet_inventory(
cls,
db: AsyncSession,
pallet_id: int,
scope: TestBatchScope,
) -> tuple[BizPallet, BizPalletInventory]:
"""按库存、托盘顺序加锁,避免与流转事务形成反序死锁。"""
inventory = await MasterDao.get_pallet_inventory(db, pallet_id, for_update=True)
inventory = await MasterDao.get_pallet_inventory(db, pallet_id, scope, for_update=True)
if inventory is None:
if await MasterDao.get_pallet(db, pallet_id) is None:
if await MasterDao.get_pallet(db, pallet_id, scope) is None:
raise ServiceException(message='托盘不存在')
raise ServiceException(message='托盘库存不存在')
pallet = await MasterDao.get_pallet(db, pallet_id, for_update=True)
assert_record_in_scope(inventory, scope, '托盘库存')
pallet = await MasterDao.get_pallet(db, pallet_id, scope, for_update=True)
if pallet is None:
raise ServiceException(message='托盘不存在')
assert_record_in_scope(pallet, scope, '托盘')
return pallet, inventory
@classmethod
@@ -75,42 +88,41 @@ class PalletService:
cls,
db: AsyncSession,
query_object: PalletPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[PalletModel]:
result = await MasterDao.list_pallet(db, query_object, is_page)
result = await MasterDao.list_pallet(db, query_object, scope, is_page)
records = result.rows if isinstance(result, PageModel) else result
models = [PalletModel.model_validate(record) for record in records]
removability = await MasterDao.get_pallet_removability(
db,
[model.pallet_id for model in models],
)
rows = [model.model_copy(update={'can_remove': removability.get(model.pallet_id, False)}) for model in models]
rows = [PalletModel.model_validate(record) for record in records]
if isinstance(result, PageModel):
return result.model_copy(update={'rows': rows})
return rows
@classmethod
async def detail_pallet(cls, db: AsyncSession, pallet_id: int) -> PalletModel:
pallet = await MasterDao.get_pallet(db, pallet_id)
async def detail_pallet(cls, db: AsyncSession, pallet_id: int, scope: TestBatchScope) -> PalletModel:
pallet = await MasterDao.get_pallet(db, pallet_id, scope)
if pallet is None:
raise ServiceException(message='托盘不存在')
model = PalletModel.model_validate(pallet)
removability = await MasterDao.get_pallet_removability(db, [pallet_id])
return model.model_copy(update={'can_remove': removability.get(pallet_id, False)})
assert_record_in_scope(pallet, scope, '托盘')
return PalletModel.model_validate(pallet)
@classmethod
async def add_pallet(
cls,
db: AsyncSession,
pallet: PalletCreateModel,
current_user_name: str,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
try:
await cls._prepare_test_write(db, current_user, scope)
if await MasterDao.find_pallet_by_code(db, pallet.pallet_code) is not None:
raise ServiceException(message='托盘编号已存在')
current_time = datetime.now()
current_user_name = current_user.user.user_name
pallet_record = BizPallet(
test_batch_id=scope.batch_id,
pallet_code=pallet.pallet_code,
qr_token=secrets.token_urlsafe(32),
capacity_packages=pallet.capacity_packages,
@@ -124,6 +136,7 @@ class PalletService:
pallet_record = await MasterDao.add_pallet(db, pallet_record)
inventory = BizPalletInventory(
pallet_id=pallet_record.pallet_id,
test_batch_id=scope.batch_id,
flow_state='UNSTOCKED',
remaining_packages=0,
version=0,
@@ -144,16 +157,19 @@ class PalletService:
cls,
db: AsyncSession,
pallet: PalletEditModel,
current_user_name: str,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
try:
_, inventory = await cls._lock_pallet_inventory(db, pallet.pallet_id)
await cls._prepare_test_write(db, current_user, scope)
_, inventory = await cls._lock_pallet_inventory(db, pallet.pallet_id, scope)
if pallet.capacity_packages < inventory.remaining_packages:
raise ServiceException(message='托盘容量不能小于当前剩余包数')
await MasterDao.update_pallet(
db,
pallet,
update_by=current_user_name,
scope,
update_by=current_user.user.user_name,
update_time=datetime.now(),
)
await db.commit()
@@ -170,27 +186,8 @@ class PalletService:
cls,
db: AsyncSession,
value: int | DeletePalletModel,
) -> CrudResponseModel:
try:
pallet_ids = cls._extract_pallet_ids(value)
for pallet_id in pallet_ids:
await cls._lock_pallet_inventory(db, pallet_id)
removability = await MasterDao.get_pallet_removability(db, pallet_ids)
if not all(removability.get(pallet_id, False) for pallet_id in pallet_ids):
raise ServiceException(message='托盘状态或关联数据不允许删除')
for pallet_id in pallet_ids:
await MasterDao.delete_pallet_inventory(db, pallet_id)
await MasterDao.delete_pallet(db, pallet_id)
await db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except IntegrityError:
await db.rollback()
raise ServiceException(message='删除托盘失败,托盘状态或关联数据已变化') from None
except Exception:
await db.rollback()
raise
) -> NoReturn:
raise ServiceWarning('', '不支持单条物理删除,请通过测试数据批次清理')
@classmethod
async def _list_resource_users(
@@ -198,12 +195,17 @@ class PalletService:
db: AsyncSession,
resource: AssignmentResource,
resource_id: int,
scope: TestBatchScope,
) -> list[BusinessUserOptionModel]:
if await getattr(MasterDao, f'get_{resource}')(db, resource_id) is None:
record = await getattr(MasterDao, f'get_{resource}')(db, resource_id, scope)
if record is None:
raise ServiceException(message=f'{_ASSIGNMENT_LABELS[resource]}不存在')
assert_record_in_scope(record, scope, _ASSIGNMENT_LABELS[resource])
users = await MasterDao.list_assignable_users(db)
assigned_user_ids = set(await getattr(MasterDao, f'list_{resource}_assigned_user_ids')(db, resource_id))
assigned_user_ids = set(
await getattr(MasterDao, f'list_{resource}_assigned_user_ids')(db, resource_id, scope)
)
return [
BusinessUserOptionModel(
user_id=user.user_id,
@@ -219,16 +221,18 @@ class PalletService:
cls,
db: AsyncSession,
warehouse_id: int,
scope: TestBatchScope,
) -> list[BusinessUserOptionModel]:
return await cls._list_resource_users(db, 'warehouse', warehouse_id)
return await cls._list_resource_users(db, 'warehouse', warehouse_id, scope)
@classmethod
async def list_site_users(
cls,
db: AsyncSession,
site_id: int,
scope: TestBatchScope,
) -> list[BusinessUserOptionModel]:
return await cls._list_resource_users(db, 'site', site_id)
return await cls._list_resource_users(db, 'site', site_id, scope)
@classmethod
async def _assign_resource_users(
@@ -237,18 +241,21 @@ class PalletService:
resource: AssignmentResource,
resource_id: int,
assignment: UserAssignModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
label = _ASSIGNMENT_LABELS[resource]
try:
if (
await getattr(MasterDao, f'get_{resource}')(
db,
resource_id,
for_update=True,
)
is None
):
await cls._prepare_test_write(db, current_user, scope)
record = await getattr(MasterDao, f'get_{resource}')(
db,
resource_id,
scope,
for_update=True,
)
if record is None:
raise ServiceException(message=f'{label}不存在')
assert_record_in_scope(record, scope, label)
users = await MasterDao.list_assignable_users_by_ids(db, assignment.user_ids)
if {user.user_id for user in users} != set(assignment.user_ids):
raise ServiceException(message='存在不存在、停用或已删除的用户')
@@ -268,8 +275,17 @@ class PalletService:
db: AsyncSession,
warehouse_id: int,
assignment: UserAssignModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._assign_resource_users(db, 'warehouse', warehouse_id, assignment)
return await cls._assign_resource_users(
db,
'warehouse',
warehouse_id,
assignment,
current_user,
scope,
)
@classmethod
async def assign_site_users(
@@ -277,5 +293,14 @@ class PalletService:
db: AsyncSession,
site_id: int,
assignment: UserAssignModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._assign_resource_users(db, 'site', site_id, assignment)
return await cls._assign_resource_users(
db,
'site',
site_id,
assignment,
current_user,
scope,
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -97,7 +97,6 @@ MASTER_MODEL_FIELDS = {
'update_by',
'update_time',
'remark',
'can_remove',
},
StrainPageQueryModel: {'page_num', 'page_size', 'strain_code', 'strain_name', 'status'},
DeleteStrainModel: {'strain_ids'},
@@ -111,7 +110,6 @@ MASTER_MODEL_FIELDS = {
'update_by',
'update_time',
'remark',
'can_remove',
},
CustomerPageQueryModel: {'page_num', 'page_size', 'customer_code', 'customer_name', 'status'},
DeleteCustomerModel: {'customer_ids'},
@@ -127,7 +125,6 @@ MASTER_MODEL_FIELDS = {
'update_by',
'update_time',
'remark',
'can_remove',
},
SitePageQueryModel: {'page_num', 'page_size', 'customer_id', 'site_code', 'site_name', 'status'},
DeleteSiteModel: {'site_ids'},
@@ -142,7 +139,6 @@ MASTER_MODEL_FIELDS = {
'update_by',
'update_time',
'remark',
'can_remove',
},
LinePageQueryModel: {'page_num', 'page_size', 'site_id', 'line_code', 'line_name', 'status'},
DeleteLineModel: {'line_ids'},
@@ -157,7 +153,6 @@ MASTER_MODEL_FIELDS = {
'update_by',
'update_time',
'remark',
'can_remove',
},
WarehousePageQueryModel: {'page_num', 'page_size', 'warehouse_code', 'warehouse_name', 'status'},
DeleteWarehouseModel: {'warehouse_ids'},
@@ -172,7 +167,6 @@ MASTER_MODEL_FIELDS = {
'create_time',
'update_by',
'update_time',
'can_remove',
},
PalletCreateModel: {'pallet_code', 'capacity_packages', 'remark'},
PalletEditModel: {'pallet_id', 'capacity_packages', 'asset_status', 'remark'},
@@ -858,6 +852,26 @@ class BusinessVoContractTest(unittest.TestCase):
with self.subTest(model=model_class.__name__):
self.assertEqual('forbid', model_class.model_config.get('extra'))
def test_master_write_models_reject_untrusted_test_batch_id(self) -> None:
payloads = (
(StrainModel, {'strainCode': 'S-1', 'strainName': '菌种'}),
(CustomerModel, {'customerCode': 'C-1', 'customerName': '客户'}),
(
SiteModel,
{'customerId': 1, 'siteCode': 'SITE-1', 'siteName': '现场'},
),
(LineModel, {'siteId': 1, 'lineCode': 'L-1', 'lineName': '产线'}),
(WarehouseModel, {'warehouseCode': 'W-1', 'warehouseName': '仓库'}),
(PalletCreateModel, {'palletCode': 'P-1', 'capacityPackages': 20}),
(
PalletEditModel,
{'palletId': 1, 'capacityPackages': 20, 'assetStatus': '0'},
),
)
for model_class, payload in payloads:
with self.subTest(model=model_class.__name__), self.assertRaises(ValidationError):
model_class.model_validate(payload | {'testBatchId': 9})
def test_all_test_batch_models_forbid_extra_fields(self) -> None:
for model_class in TEST_BATCH_MODEL_FIELDS:
with self.subTest(model=model_class.__name__):