功能:新增业务基础资料接口

This commit is contained in:
焦龙言
2026-07-26 10:41:14 +08:00
parent f5d47ad2bb
commit f3eda856b0
8 changed files with 2114 additions and 0 deletions
@@ -0,0 +1,123 @@
from datetime import datetime
from typing import Annotated
from fastapi import Path, Query, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from common.annotation.log_annotation import Log
from common.aspect.db_seesion import DBSessionDependency
from common.aspect.interface_auth import UserInterfaceAuthDependency
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
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.entity.vo.master_vo import CustomerModel, CustomerPageQueryModel, DeleteCustomerModel
from module_business.service.master_service import MasterService
from utils.response_util import ResponseUtil
customer_controller = APIRouterPro(
prefix='/master/customer', tags=['基础资料-客户管理'], dependencies=[PreAuthDependency()]
)
@customer_controller.get(
'/list',
summary='获取客户分页列表接口',
description='用于获取客户分页列表',
response_model=PageResponseModel[CustomerModel],
dependencies=[UserInterfaceAuthDependency('master:customer:list')],
)
async def get_master_customer_list(
request: Request,
customer_page_query: Annotated[CustomerPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
customer_page_query_result = await MasterService.list_customer(query_db, customer_page_query, is_page=True)
return ResponseUtil.success(model_content=customer_page_query_result)
@customer_controller.get(
'/{customer_id}',
summary='获取客户详情接口',
description='用于获取指定客户的详细信息',
response_model=DataResponseModel[CustomerModel],
dependencies=[UserInterfaceAuthDependency('master:customer:query')],
)
async def query_detail_master_customer(
request: Request,
customer_id: Annotated[int, Path(gt=0, description='客户ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
customer_detail_result = await MasterService.detail_customer(query_db, customer_id)
return ResponseUtil.success(data=customer_detail_result)
@customer_controller.post(
'',
summary='新增客户接口',
description='用于新增客户',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:customer:add')],
)
@Log(title='客户管理', business_type=BusinessType.INSERT)
async def add_master_customer(
request: Request,
add_customer: CustomerModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=add_customer_result.message)
@customer_controller.put(
'',
summary='编辑客户接口',
description='用于编辑客户',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:customer:edit')],
)
@Log(title='客户管理', business_type=BusinessType.UPDATE)
async def edit_master_customer(
request: Request,
edit_customer: CustomerModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=edit_customer_result.message)
@customer_controller.delete(
'/{customer_ids}',
summary='删除客户接口',
description='用于删除客户',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:customer:remove')],
)
@Log(title='客户管理', business_type=BusinessType.DELETE)
async def delete_master_customer(
request: Request,
customer_ids: Annotated[
str,
Path(pattern=r'^[1-9]\d*(,[1-9]\d*)*$', description='需要删除的客户ID'),
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
delete_customer = DeleteCustomerModel(customerIds=[int(customer_id) for customer_id in customer_ids.split(',')])
delete_customer_result = await MasterService.remove_customer(query_db, delete_customer)
return ResponseUtil.success(msg=delete_customer_result.message)
@@ -0,0 +1,121 @@
from datetime import datetime
from typing import Annotated
from fastapi import Path, Query, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from common.annotation.log_annotation import Log
from common.aspect.db_seesion import DBSessionDependency
from common.aspect.interface_auth import UserInterfaceAuthDependency
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
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.entity.vo.master_vo import DeleteLineModel, LineModel, LinePageQueryModel
from module_business.service.master_service import MasterService
from utils.response_util import ResponseUtil
line_controller = APIRouterPro(prefix='/master/line', tags=['基础资料-生产线管理'], dependencies=[PreAuthDependency()])
@line_controller.get(
'/list',
summary='获取生产线分页列表接口',
description='用于获取生产线分页列表',
response_model=PageResponseModel[LineModel],
dependencies=[UserInterfaceAuthDependency('master:line:list')],
)
async def get_master_line_list(
request: Request,
line_page_query: Annotated[LinePageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
line_page_query_result = await MasterService.list_line(query_db, line_page_query, is_page=True)
return ResponseUtil.success(model_content=line_page_query_result)
@line_controller.get(
'/{line_id}',
summary='获取生产线详情接口',
description='用于获取指定生产线的详细信息',
response_model=DataResponseModel[LineModel],
dependencies=[UserInterfaceAuthDependency('master:line:query')],
)
async def query_detail_master_line(
request: Request,
line_id: Annotated[int, Path(gt=0, description='生产线ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
line_detail_result = await MasterService.detail_line(query_db, line_id)
return ResponseUtil.success(data=line_detail_result)
@line_controller.post(
'',
summary='新增生产线接口',
description='用于新增生产线',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:line:add')],
)
@Log(title='生产线管理', business_type=BusinessType.INSERT)
async def add_master_line(
request: Request,
add_line: LineModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=add_line_result.message)
@line_controller.put(
'',
summary='编辑生产线接口',
description='用于编辑生产线',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:line:edit')],
)
@Log(title='生产线管理', business_type=BusinessType.UPDATE)
async def edit_master_line(
request: Request,
edit_line: LineModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=edit_line_result.message)
@line_controller.delete(
'/{line_ids}',
summary='删除生产线接口',
description='用于删除生产线',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:line:remove')],
)
@Log(title='生产线管理', business_type=BusinessType.DELETE)
async def delete_master_line(
request: Request,
line_ids: Annotated[
str,
Path(pattern=r'^[1-9]\d*(,[1-9]\d*)*$', description='需要删除的生产线ID'),
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
delete_line = DeleteLineModel(lineIds=[int(line_id) for line_id in line_ids.split(',')])
delete_line_result = await MasterService.remove_line(query_db, delete_line)
return ResponseUtil.success(msg=delete_line_result.message)
@@ -0,0 +1,123 @@
from datetime import datetime
from typing import Annotated
from fastapi import Path, Query, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from common.annotation.log_annotation import Log
from common.aspect.db_seesion import DBSessionDependency
from common.aspect.interface_auth import UserInterfaceAuthDependency
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
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.entity.vo.master_vo import DeleteSiteModel, SiteModel, SitePageQueryModel
from module_business.service.master_service import MasterService
from utils.response_util import ResponseUtil
site_controller = APIRouterPro(
prefix='/master/site', tags=['基础资料-客户现场管理'], dependencies=[PreAuthDependency()]
)
@site_controller.get(
'/list',
summary='获取客户现场分页列表接口',
description='用于获取客户现场分页列表',
response_model=PageResponseModel[SiteModel],
dependencies=[UserInterfaceAuthDependency('master:site:list')],
)
async def get_master_site_list(
request: Request,
site_page_query: Annotated[SitePageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
site_page_query_result = await MasterService.list_site(query_db, site_page_query, is_page=True)
return ResponseUtil.success(model_content=site_page_query_result)
@site_controller.get(
'/{site_id}',
summary='获取客户现场详情接口',
description='用于获取指定客户现场的详细信息',
response_model=DataResponseModel[SiteModel],
dependencies=[UserInterfaceAuthDependency('master:site:query')],
)
async def query_detail_master_site(
request: Request,
site_id: Annotated[int, Path(gt=0, description='客户现场ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
site_detail_result = await MasterService.detail_site(query_db, site_id)
return ResponseUtil.success(data=site_detail_result)
@site_controller.post(
'',
summary='新增客户现场接口',
description='用于新增客户现场',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:site:add')],
)
@Log(title='客户现场管理', business_type=BusinessType.INSERT)
async def add_master_site(
request: Request,
add_site: SiteModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=add_site_result.message)
@site_controller.put(
'',
summary='编辑客户现场接口',
description='用于编辑客户现场',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:site:edit')],
)
@Log(title='客户现场管理', business_type=BusinessType.UPDATE)
async def edit_master_site(
request: Request,
edit_site: SiteModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=edit_site_result.message)
@site_controller.delete(
'/{site_ids}',
summary='删除客户现场接口',
description='用于删除客户现场',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:site:remove')],
)
@Log(title='客户现场管理', business_type=BusinessType.DELETE)
async def delete_master_site(
request: Request,
site_ids: Annotated[
str,
Path(pattern=r'^[1-9]\d*(,[1-9]\d*)*$', description='需要删除的客户现场ID'),
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
delete_site = DeleteSiteModel(siteIds=[int(site_id) for site_id in site_ids.split(',')])
delete_site_result = await MasterService.remove_site(query_db, delete_site)
return ResponseUtil.success(msg=delete_site_result.message)
@@ -0,0 +1,123 @@
from datetime import datetime
from typing import Annotated
from fastapi import Path, Query, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from common.annotation.log_annotation import Log
from common.aspect.db_seesion import DBSessionDependency
from common.aspect.interface_auth import UserInterfaceAuthDependency
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
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.entity.vo.master_vo import DeleteStrainModel, StrainModel, StrainPageQueryModel
from module_business.service.master_service import MasterService
from utils.response_util import ResponseUtil
strain_controller = APIRouterPro(
prefix='/master/strain', tags=['基础资料-菌种管理'], dependencies=[PreAuthDependency()]
)
@strain_controller.get(
'/list',
summary='获取菌种分页列表接口',
description='用于获取菌种分页列表',
response_model=PageResponseModel[StrainModel],
dependencies=[UserInterfaceAuthDependency('master:strain:list')],
)
async def get_master_strain_list(
request: Request,
strain_page_query: Annotated[StrainPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
strain_page_query_result = await MasterService.list_strain(query_db, strain_page_query, is_page=True)
return ResponseUtil.success(model_content=strain_page_query_result)
@strain_controller.get(
'/{strain_id}',
summary='获取菌种详情接口',
description='用于获取指定菌种的详细信息',
response_model=DataResponseModel[StrainModel],
dependencies=[UserInterfaceAuthDependency('master:strain:query')],
)
async def query_detail_master_strain(
request: Request,
strain_id: Annotated[int, Path(gt=0, description='菌种ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
strain_detail_result = await MasterService.detail_strain(query_db, strain_id)
return ResponseUtil.success(data=strain_detail_result)
@strain_controller.post(
'',
summary='新增菌种接口',
description='用于新增菌种',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:strain:add')],
)
@Log(title='菌种管理', business_type=BusinessType.INSERT)
async def add_master_strain(
request: Request,
add_strain: StrainModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=add_strain_result.message)
@strain_controller.put(
'',
summary='编辑菌种接口',
description='用于编辑菌种',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:strain:edit')],
)
@Log(title='菌种管理', business_type=BusinessType.UPDATE)
async def edit_master_strain(
request: Request,
edit_strain: StrainModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=edit_strain_result.message)
@strain_controller.delete(
'/{strain_ids}',
summary='删除菌种接口',
description='用于删除菌种',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:strain:remove')],
)
@Log(title='菌种管理', business_type=BusinessType.DELETE)
async def delete_master_strain(
request: Request,
strain_ids: Annotated[
str,
Path(pattern=r'^[1-9]\d*(,[1-9]\d*)*$', description='需要删除的菌种ID'),
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
delete_strain = DeleteStrainModel(strainIds=[int(strain_id) for strain_id in strain_ids.split(',')])
delete_strain_result = await MasterService.remove_strain(query_db, delete_strain)
return ResponseUtil.success(msg=delete_strain_result.message)
@@ -0,0 +1,125 @@
from datetime import datetime
from typing import Annotated
from fastapi import Path, Query, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from common.annotation.log_annotation import Log
from common.aspect.db_seesion import DBSessionDependency
from common.aspect.interface_auth import UserInterfaceAuthDependency
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
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.entity.vo.master_vo import DeleteWarehouseModel, WarehouseModel, WarehousePageQueryModel
from module_business.service.master_service import MasterService
from utils.response_util import ResponseUtil
warehouse_controller = APIRouterPro(
prefix='/master/warehouse', tags=['基础资料-仓库管理'], dependencies=[PreAuthDependency()]
)
@warehouse_controller.get(
'/list',
summary='获取仓库分页列表接口',
description='用于获取仓库分页列表',
response_model=PageResponseModel[WarehouseModel],
dependencies=[UserInterfaceAuthDependency('master:warehouse:list')],
)
async def get_master_warehouse_list(
request: Request,
warehouse_page_query: Annotated[WarehousePageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
warehouse_page_query_result = await MasterService.list_warehouse(query_db, warehouse_page_query, is_page=True)
return ResponseUtil.success(model_content=warehouse_page_query_result)
@warehouse_controller.get(
'/{warehouse_id}',
summary='获取仓库详情接口',
description='用于获取指定仓库的详细信息',
response_model=DataResponseModel[WarehouseModel],
dependencies=[UserInterfaceAuthDependency('master:warehouse:query')],
)
async def query_detail_master_warehouse(
request: Request,
warehouse_id: Annotated[int, Path(gt=0, description='仓库ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
warehouse_detail_result = await MasterService.detail_warehouse(query_db, warehouse_id)
return ResponseUtil.success(data=warehouse_detail_result)
@warehouse_controller.post(
'',
summary='新增仓库接口',
description='用于新增仓库',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:warehouse:add')],
)
@Log(title='仓库管理', business_type=BusinessType.INSERT)
async def add_master_warehouse(
request: Request,
add_warehouse: WarehouseModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=add_warehouse_result.message)
@warehouse_controller.put(
'',
summary='编辑仓库接口',
description='用于编辑仓库',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:warehouse:edit')],
)
@Log(title='仓库管理', business_type=BusinessType.UPDATE)
async def edit_master_warehouse(
request: Request,
edit_warehouse: WarehouseModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> 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)
return ResponseUtil.success(msg=edit_warehouse_result.message)
@warehouse_controller.delete(
'/{warehouse_ids}',
summary='删除仓库接口',
description='用于删除仓库',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('master:warehouse:remove')],
)
@Log(title='仓库管理', business_type=BusinessType.DELETE)
async def delete_master_warehouse(
request: Request,
warehouse_ids: Annotated[
str,
Path(pattern=r'^[1-9]\d*(,[1-9]\d*)*$', description='需要删除的仓库ID'),
],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
delete_warehouse = DeleteWarehouseModel(
warehouseIds=[int(warehouse_id) for warehouse_id in warehouse_ids.split(',')]
)
delete_warehouse_result = await MasterService.remove_warehouse(query_db, delete_warehouse)
return ResponseUtil.success(msg=delete_warehouse_result.message)
@@ -0,0 +1,378 @@
from typing import Any, Literal
from pydantic import BaseModel
from sqlalchemy import ColumnElement, Select, delete, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel
from module_business.entity.do.master_do import (
BizCustomer,
BizCustomerSite,
BizProductionLine,
BizStrain,
BizUserSite,
BizUserWarehouse,
BizWarehouse,
)
from module_business.entity.do.operation_do import (
BizFillingRecord,
BizInboundOrder,
BizPalletEvent,
BizPalletInventory,
BizReturnOrder,
BizShipmentOrder,
BizUsageRecord,
)
from module_business.entity.vo.master_vo import (
CustomerModel,
CustomerPageQueryModel,
LineModel,
LinePageQueryModel,
SiteModel,
SitePageQueryModel,
StrainModel,
StrainPageQueryModel,
WarehouseModel,
WarehousePageQueryModel,
)
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),
),
}
class MasterDao:
"""菌种、客户、现场、产线和仓库主数据数据库操作层。"""
@staticmethod
async def _paginate(
db: AsyncSession,
statement: Select,
query_object: BaseModel,
is_page: bool,
) -> PageModel | list[dict[str, Any]]:
return await PageUtil.paginate(
db,
statement,
query_object.page_num,
query_object.page_size,
is_page,
)
@staticmethod
async def _get(
db: AsyncSession,
model: type[Any],
id_column: InstrumentedAttribute,
resource_id: int,
) -> Any | None:
result = await db.execute(select(model).where(id_column == resource_id))
return result.scalar_one_or_none()
@staticmethod
async def _find(
db: AsyncSession,
model: type[Any],
code_column: InstrumentedAttribute,
code: str,
*scope_predicates: ColumnElement[bool],
) -> Any | None:
result = await db.execute(select(model).where(code_column == code, *scope_predicates))
return result.scalar_one_or_none()
@staticmethod
async def _add(
db: AsyncSession,
model: type[Any],
id_field: str,
page_object: BaseModel,
) -> Any:
payload = page_object.model_dump(exclude={id_field})
record = model(**payload)
db.add(record)
await db.flush()
return record
@staticmethod
async def _update(
db: AsyncSession,
model: type[Any],
id_column: InstrumentedAttribute,
id_field: str,
page_object: BaseModel,
) -> None:
payload = page_object.model_dump(
exclude_unset=True,
exclude={'create_by', 'create_time'},
)
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 list_strain(
cls,
db: AsyncSession,
query_object: StrainPageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
if query_object.strain_code:
conditions.append(BizStrain.strain_code.like(f'%{query_object.strain_code}%'))
if query_object.strain_name:
conditions.append(BizStrain.strain_name.like(f'%{query_object.strain_name}%'))
if query_object.status:
conditions.append(BizStrain.status == query_object.status)
statement = select(BizStrain).where(*conditions).order_by(BizStrain.strain_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_strain(cls, db: AsyncSession, strain_id: int) -> BizStrain | None:
return await cls._get(db, BizStrain, BizStrain.strain_id, strain_id)
@classmethod
async def find_strain_by_code(cls, db: AsyncSession, strain_code: str) -> BizStrain | None:
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)
@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)
@classmethod
async def list_customer(
cls,
db: AsyncSession,
query_object: CustomerPageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
if query_object.customer_code:
conditions.append(BizCustomer.customer_code.like(f'%{query_object.customer_code}%'))
if query_object.customer_name:
conditions.append(BizCustomer.customer_name.like(f'%{query_object.customer_name}%'))
if query_object.status:
conditions.append(BizCustomer.status == query_object.status)
statement = select(BizCustomer).where(*conditions).order_by(BizCustomer.customer_id)
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)
@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)
@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)
@classmethod
async def list_site(
cls,
db: AsyncSession,
query_object: SitePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
if query_object.customer_id:
conditions.append(BizCustomerSite.customer_id == query_object.customer_id)
if query_object.site_code:
conditions.append(BizCustomerSite.site_code.like(f'%{query_object.site_code}%'))
if query_object.site_name:
conditions.append(BizCustomerSite.site_name.like(f'%{query_object.site_name}%'))
if query_object.status:
conditions.append(BizCustomerSite.status == query_object.status)
statement = select(BizCustomerSite).where(*conditions).order_by(BizCustomerSite.site_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_site(cls, db: AsyncSession, site_id: int) -> BizCustomerSite | None:
return await cls._get(db, BizCustomerSite, BizCustomerSite.site_id, site_id)
@classmethod
async def find_site_by_code(
cls,
db: AsyncSession,
site_code: str,
customer_id: int,
) -> BizCustomerSite | None:
return await cls._find(
db,
BizCustomerSite,
BizCustomerSite.site_code,
site_code,
BizCustomerSite.customer_id == customer_id,
)
@classmethod
async def add_site(cls, db: AsyncSession, site: SiteModel) -> BizCustomerSite:
return await cls._add(db, BizCustomerSite, 'site_id', site)
@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)
@classmethod
async def list_line(
cls,
db: AsyncSession,
query_object: LinePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
if query_object.site_id:
conditions.append(BizProductionLine.site_id == query_object.site_id)
if query_object.line_code:
conditions.append(BizProductionLine.line_code.like(f'%{query_object.line_code}%'))
if query_object.line_name:
conditions.append(BizProductionLine.line_name.like(f'%{query_object.line_name}%'))
if query_object.status:
conditions.append(BizProductionLine.status == query_object.status)
statement = select(BizProductionLine).where(*conditions).order_by(BizProductionLine.line_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_line(cls, db: AsyncSession, line_id: int) -> BizProductionLine | None:
return await cls._get(db, BizProductionLine, BizProductionLine.line_id, line_id)
@classmethod
async def find_line_by_code(
cls,
db: AsyncSession,
line_code: str,
site_id: int,
) -> BizProductionLine | None:
return await cls._find(
db,
BizProductionLine,
BizProductionLine.line_code,
line_code,
BizProductionLine.site_id == site_id,
)
@classmethod
async def add_line(cls, db: AsyncSession, line: LineModel) -> BizProductionLine:
return await cls._add(db, BizProductionLine, 'line_id', line)
@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)
@classmethod
async def list_warehouse(
cls,
db: AsyncSession,
query_object: WarehousePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = []
if query_object.warehouse_code:
conditions.append(BizWarehouse.warehouse_code.like(f'%{query_object.warehouse_code}%'))
if query_object.warehouse_name:
conditions.append(BizWarehouse.warehouse_name.like(f'%{query_object.warehouse_name}%'))
if query_object.status:
conditions.append(BizWarehouse.status == query_object.status)
statement = select(BizWarehouse).where(*conditions).order_by(BizWarehouse.warehouse_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_warehouse(cls, db: AsyncSession, warehouse_id: int) -> BizWarehouse | None:
return await cls._get(db, BizWarehouse, BizWarehouse.warehouse_id, warehouse_id)
@classmethod
async def find_warehouse_by_code(cls, db: AsyncSession, warehouse_code: str) -> BizWarehouse | None:
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)
@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)
@classmethod
async def count_references(
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}')
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
@@ -0,0 +1,375 @@
from typing import Any, Literal
from pydantic import BaseModel
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException, ServiceWarning
from module_business.dao.master_dao import MasterDao
from module_business.entity.vo.master_vo import (
CustomerModel,
CustomerPageQueryModel,
DeleteCustomerModel,
DeleteLineModel,
DeleteSiteModel,
DeleteStrainModel,
DeleteWarehouseModel,
LineModel,
LinePageQueryModel,
SiteModel,
SitePageQueryModel,
StrainModel,
StrainPageQueryModel,
WarehouseModel,
WarehousePageQueryModel,
)
MasterResource = Literal['strain', 'customer', 'site', 'line', 'warehouse']
_RESOURCE_CONFIG: dict[MasterResource, dict[str, Any]] = {
'strain': {
'label': '菌种',
'id_field': 'strain_id',
'code_field': 'strain_code',
'ids_field': 'strain_ids',
'vo': StrainModel,
},
'customer': {
'label': '客户',
'id_field': 'customer_id',
'code_field': 'customer_code',
'ids_field': 'customer_ids',
'vo': CustomerModel,
},
'site': {
'label': '客户现场',
'id_field': 'site_id',
'code_field': 'site_code',
'ids_field': 'site_ids',
'vo': SiteModel,
},
'line': {
'label': '生产线',
'id_field': 'line_id',
'code_field': 'line_code',
'ids_field': 'line_ids',
'vo': LineModel,
},
'warehouse': {
'label': '仓库',
'id_field': 'warehouse_id',
'code_field': 'warehouse_code',
'ids_field': 'warehouse_ids',
'vo': WarehouseModel,
},
}
class MasterService:
"""菌种、客户、现场、产线和仓库主数据服务层。"""
@staticmethod
async def _validate_parent(db: AsyncSession, resource: MasterResource, page_object: BaseModel) -> None:
if resource == 'site':
customer = await MasterDao.get_customer(db, page_object.customer_id)
if customer is None:
raise ServiceException(message='目标客户不存在')
if customer.status != '0':
raise ServiceException(message='目标客户已停用')
elif resource == 'line':
site = await MasterDao.get_site(db, page_object.site_id)
if site is None:
raise ServiceException(message='目标客户现场不存在')
if site.status != '0':
raise ServiceException(message='目标客户现场已停用')
@staticmethod
async def _find_duplicate(
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
) -> Any | None:
if resource == 'site':
return await MasterDao.find_site_by_code(
db,
page_object.site_code,
page_object.customer_id,
)
if resource == 'line':
return await MasterDao.find_line_by_code(
db,
page_object.line_code,
page_object.site_id,
)
code_field = _RESOURCE_CONFIG[resource]['code_field']
return await getattr(MasterDao, f'find_{resource}_by_code')(
db,
getattr(page_object, code_field),
)
@classmethod
async def _validate_unique(
cls,
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
current_id: int | None = None,
) -> None:
duplicate = await cls._find_duplicate(db, resource, page_object)
if duplicate is None:
return
config = _RESOURCE_CONFIG[resource]
duplicate_id = getattr(duplicate, config['id_field'], None)
if current_id is None or duplicate_id != current_id:
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,
is_page: bool,
) -> PageModel | list[dict[str, Any]]:
return await getattr(MasterDao, f'list_{resource}')(db, query_object, is_page)
@classmethod
async def _detail(cls, db: AsyncSession, resource: MasterResource, resource_id: int) -> BaseModel:
config = _RESOURCE_CONFIG[resource]
record = await getattr(MasterDao, f'get_{resource}')(db, resource_id)
if record is None:
raise ServiceException(message=f'{config["label"]}不存在')
return config['vo'].model_validate(record)
@classmethod
async def _add(
cls,
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
) -> CrudResponseModel:
config = _RESOURCE_CONFIG[resource]
try:
await cls._validate_parent(db, resource, page_object)
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 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
@classmethod
async def _edit(
cls,
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
) -> CrudResponseModel:
config = _RESOURCE_CONFIG[resource]
try:
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)
if current is None:
raise ServiceException(message=f'{config["label"]}不存在')
await cls._validate_parent(db, resource, page_object)
await cls._validate_unique(db, resource, page_object, resource_id)
await getattr(MasterDao, f'update_{resource}')(db, page_object)
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
@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
@classmethod
async def list_strain(
cls,
db: AsyncSession,
query_object: StrainPageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
return await cls._list(db, 'strain', query_object, is_page)
@classmethod
async def detail_strain(cls, db: AsyncSession, strain_id: int) -> StrainModel:
return await cls._detail(db, 'strain', strain_id)
@classmethod
async def add_strain(cls, db: AsyncSession, strain: StrainModel) -> CrudResponseModel:
return await cls._add(db, 'strain', strain)
@classmethod
async def edit_strain(cls, db: AsyncSession, strain: StrainModel) -> CrudResponseModel:
return await cls._edit(db, 'strain', strain)
@classmethod
async def remove_strain(
cls,
db: AsyncSession,
value: int | DeleteStrainModel,
) -> CrudResponseModel:
return await cls._remove(db, 'strain', value)
@classmethod
async def list_customer(
cls,
db: AsyncSession,
query_object: CustomerPageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
return await cls._list(db, 'customer', query_object, is_page)
@classmethod
async def detail_customer(cls, db: AsyncSession, customer_id: int) -> CustomerModel:
return await cls._detail(db, 'customer', customer_id)
@classmethod
async def add_customer(cls, db: AsyncSession, customer: CustomerModel) -> CrudResponseModel:
return await cls._add(db, 'customer', customer)
@classmethod
async def edit_customer(cls, db: AsyncSession, customer: CustomerModel) -> CrudResponseModel:
return await cls._edit(db, 'customer', customer)
@classmethod
async def remove_customer(
cls,
db: AsyncSession,
value: int | DeleteCustomerModel,
) -> CrudResponseModel:
return await cls._remove(db, 'customer', value)
@classmethod
async def list_site(
cls,
db: AsyncSession,
query_object: SitePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
return await cls._list(db, 'site', query_object, is_page)
@classmethod
async def detail_site(cls, db: AsyncSession, site_id: int) -> SiteModel:
return await cls._detail(db, 'site', site_id)
@classmethod
async def add_site(cls, db: AsyncSession, site: SiteModel) -> CrudResponseModel:
return await cls._add(db, 'site', site)
@classmethod
async def edit_site(cls, db: AsyncSession, site: SiteModel) -> CrudResponseModel:
return await cls._edit(db, 'site', site)
@classmethod
async def remove_site(
cls,
db: AsyncSession,
value: int | DeleteSiteModel,
) -> CrudResponseModel:
return await cls._remove(db, 'site', value)
@classmethod
async def list_line(
cls,
db: AsyncSession,
query_object: LinePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
return await cls._list(db, 'line', query_object, is_page)
@classmethod
async def detail_line(cls, db: AsyncSession, line_id: int) -> LineModel:
return await cls._detail(db, 'line', line_id)
@classmethod
async def add_line(cls, db: AsyncSession, line: LineModel) -> CrudResponseModel:
return await cls._add(db, 'line', line)
@classmethod
async def edit_line(cls, db: AsyncSession, line: LineModel) -> CrudResponseModel:
return await cls._edit(db, 'line', line)
@classmethod
async def remove_line(
cls,
db: AsyncSession,
value: int | DeleteLineModel,
) -> CrudResponseModel:
return await cls._remove(db, 'line', value)
@classmethod
async def list_warehouse(
cls,
db: AsyncSession,
query_object: WarehousePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
return await cls._list(db, 'warehouse', query_object, is_page)
@classmethod
async def detail_warehouse(cls, db: AsyncSession, warehouse_id: int) -> WarehouseModel:
return await cls._detail(db, 'warehouse', warehouse_id)
@classmethod
async def add_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel) -> CrudResponseModel:
return await cls._add(db, 'warehouse', warehouse)
@classmethod
async def edit_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel) -> CrudResponseModel:
return await cls._edit(db, 'warehouse', warehouse)
@classmethod
async def remove_warehouse(
cls,
db: AsyncSession,
value: int | DeleteWarehouseModel,
) -> CrudResponseModel:
return await cls._remove(db, 'warehouse', value)
@@ -0,0 +1,746 @@
import inspect
import unittest
from contextlib import ExitStack
from datetime import datetime
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import FastAPI
from fastapi.routing import APIRoute
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from common.aspect.interface_auth import CheckUserInterfaceAuth
from common.aspect.pre_auth import PreAuth
from common.vo import CrudResponseModel, DataResponseModel, PageModel, PageResponseModel
from exceptions.exception import ServiceException, ServiceWarning
from module_admin.service.login_service import LoginService
from module_business.controller import (
customer_controller as customer_controller_module,
)
from module_business.controller import line_controller as line_controller_module
from module_business.controller import site_controller as site_controller_module
from module_business.controller import strain_controller as strain_controller_module
from module_business.controller import warehouse_controller as warehouse_controller_module
from module_business.dao.master_dao import MasterDao
from module_business.entity.do.master_do import (
BizCustomer,
BizCustomerSite,
BizProductionLine,
BizStrain,
BizWarehouse,
)
from module_business.entity.vo.master_vo import (
CustomerModel,
CustomerPageQueryModel,
DeleteCustomerModel,
DeleteLineModel,
DeleteSiteModel,
DeleteStrainModel,
DeleteWarehouseModel,
LineModel,
LinePageQueryModel,
SiteModel,
SitePageQueryModel,
StrainModel,
StrainPageQueryModel,
WarehouseModel,
WarehousePageQueryModel,
)
from module_business.service.master_service import MasterService
from utils.page_util import PageUtil
RESOURCE_IDS = {
'strain': 'strain_id',
'customer': 'customer_id',
'site': 'site_id',
'line': 'line_id',
'warehouse': 'warehouse_id',
}
def make_models(resource_id: int | None = None) -> dict[str, object]:
return {
'strain': StrainModel(strainId=resource_id, strainCode='S001', strainName='测试菌种'),
'customer': CustomerModel(customerId=resource_id, customerCode='C001', customerName='测试客户'),
'site': SiteModel(
siteId=resource_id,
customerId=11,
siteCode='SITE001',
siteName='测试现场',
),
'line': LineModel(
lineId=resource_id,
siteId=22,
lineCode='L001',
lineName='测试产线',
),
'warehouse': WarehouseModel(
warehouseId=resource_id,
warehouseCode='W001',
warehouseName='测试仓库',
),
}
def compile_sql(statement: Any) -> str:
return ' '.join(str(statement.compile(compile_kwargs={'literal_binds': True})).split())
def integrity_error() -> IntegrityError:
return IntegrityError('statement', {}, RuntimeError('duplicate'))
class TestMasterService(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
self.db = MagicMock(spec=AsyncSession)
self.strain = StrainModel(strainCode='S001', strainName='测试菌种')
self.line = LineModel(siteId=1, lineCode='L001', lineName='测试产线')
def _patch_enabled_parent(self, stack: ExitStack, resource: str) -> None:
if resource == 'site':
stack.enter_context(
patch.object(MasterDao, 'get_customer', new=AsyncMock(return_value=SimpleNamespace(status='0')))
)
elif resource == 'line':
stack.enter_context(
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0')))
)
async def test_duplicate_strain_code_is_rejected(self) -> None:
with (
patch.object(MasterDao, 'find_strain_by_code', new=AsyncMock(return_value=object())),
self.assertRaises(ServiceException),
):
await MasterService.add_strain(self.db, self.strain)
async def test_line_must_belong_to_enabled_site(self) -> None:
with (
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=None)),
self.assertRaises(ServiceException),
):
await MasterService.add_line(self.db, self.line)
async def test_referenced_master_can_only_be_disabled(self) -> None:
with (
patch.object(MasterDao, 'count_references', new=AsyncMock(return_value=1)),
self.assertRaises(ServiceWarning),
):
await MasterService.remove_strain(self.db, 1)
async def test_duplicate_code_is_rejected_for_all_five_resources(self) -> None:
for resource, model in make_models().items():
with self.subTest(resource=resource), ExitStack() as stack:
self._patch_enabled_parent(stack, resource)
stack.enter_context(
patch.object(
MasterDao,
f'find_{resource}_by_code',
new=AsyncMock(return_value=SimpleNamespace()),
)
)
with self.assertRaises(ServiceException):
await getattr(MasterService, f'add_{resource}')(self.db, model)
async def test_edit_unique_check_excludes_current_record_for_all_resources(self) -> None:
for resource, model in make_models(7).items():
id_field = RESOURCE_IDS[resource]
current = SimpleNamespace(**{id_field: 7})
with self.subTest(resource=resource), ExitStack() as stack:
self.db.reset_mock()
self._patch_enabled_parent(stack, resource)
stack.enter_context(patch.object(MasterDao, f'get_{resource}', new=AsyncMock(return_value=current)))
stack.enter_context(
patch.object(MasterDao, f'find_{resource}_by_code', new=AsyncMock(return_value=current))
)
update_mock = stack.enter_context(patch.object(MasterDao, f'update_{resource}', new=AsyncMock()))
result = await getattr(MasterService, f'edit_{resource}')(self.db, model)
self.assertIsInstance(result, CrudResponseModel)
self.assertTrue(result.is_success)
update_mock.assert_awaited_once()
self.db.commit.assert_awaited_once()
async def test_edit_duplicate_code_from_another_record_is_rejected_for_all_resources(self) -> None:
for resource, model in make_models(7).items():
id_field = RESOURCE_IDS[resource]
with self.subTest(resource=resource), ExitStack() as stack:
self._patch_enabled_parent(stack, resource)
stack.enter_context(
patch.object(
MasterDao,
f'get_{resource}',
new=AsyncMock(return_value=SimpleNamespace(**{id_field: 7})),
)
)
stack.enter_context(
patch.object(
MasterDao,
f'find_{resource}_by_code',
new=AsyncMock(return_value=SimpleNamespace(**{id_field: 8})),
)
)
with self.assertRaises(ServiceException):
await getattr(MasterService, f'edit_{resource}')(self.db, model)
async def test_site_add_requires_existing_enabled_customer(self) -> None:
site = make_models()['site']
for parent in (None, SimpleNamespace(status='1')):
with (
self.subTest(parent=parent),
patch.object(MasterDao, 'get_customer', new=AsyncMock(return_value=parent)),
self.assertRaises(ServiceException),
):
await MasterService.add_site(self.db, site)
with (
patch.object(
MasterDao,
'get_customer',
new=AsyncMock(return_value=SimpleNamespace(status='0')),
),
patch.object(MasterDao, 'find_site_by_code', new=AsyncMock(return_value=None)),
patch.object(MasterDao, 'add_site', new=AsyncMock()),
):
result = await MasterService.add_site(self.db, site)
self.assertTrue(result.is_success)
async def test_line_add_requires_existing_enabled_site(self) -> None:
line = make_models()['line']
for parent in (None, SimpleNamespace(status='1')):
with (
self.subTest(parent=parent),
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=parent)),
self.assertRaises(ServiceException),
):
await MasterService.add_line(self.db, line)
with (
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
patch.object(MasterDao, 'find_line_by_code', new=AsyncMock(return_value=None)),
patch.object(MasterDao, 'add_line', new=AsyncMock()),
):
result = await MasterService.add_line(self.db, line)
self.assertTrue(result.is_success)
async def test_site_and_line_edit_require_enabled_target_parent(self) -> None:
cases = (
('site', make_models(7)['site'], 'get_customer'),
('line', make_models(7)['line'], 'get_site'),
)
for resource, model, parent_method in cases:
for parent in (None, SimpleNamespace(status='1')):
with self.subTest(resource=resource, parent=parent), ExitStack() as stack:
stack.enter_context(
patch.object(
MasterDao,
f'get_{resource}',
new=AsyncMock(return_value=SimpleNamespace(**{RESOURCE_IDS[resource]: 7})),
)
)
stack.enter_context(patch.object(MasterDao, parent_method, new=AsyncMock(return_value=parent)))
with self.assertRaises(ServiceException):
await getattr(MasterService, f'edit_{resource}')(self.db, model)
with self.subTest(resource=resource, parent='enabled'), ExitStack() as stack:
stack.enter_context(
patch.object(
MasterDao,
f'get_{resource}',
new=AsyncMock(return_value=SimpleNamespace(**{RESOURCE_IDS[resource]: 7})),
)
)
stack.enter_context(
patch.object(
MasterDao,
parent_method,
new=AsyncMock(return_value=SimpleNamespace(status='0')),
)
)
stack.enter_context(
patch.object(MasterDao, f'find_{resource}_by_code', new=AsyncMock(return_value=None))
)
stack.enter_context(patch.object(MasterDao, f'update_{resource}', new=AsyncMock()))
result = await getattr(MasterService, f'edit_{resource}')(self.db, model)
self.assertTrue(result.is_success)
async def test_detail_missing_is_explicit_for_all_resources(self) -> None:
for resource in RESOURCE_IDS:
with (
self.subTest(resource=resource),
patch.object(MasterDao, f'get_{resource}', new=AsyncMock(return_value=None)),
self.assertRaises(ServiceException),
):
await getattr(MasterService, f'detail_{resource}')(self.db, 99)
async def test_reference_delete_protection_covers_all_resources(self) -> None:
for resource in RESOURCE_IDS:
with (
self.subTest(resource=resource),
patch.object(MasterDao, 'count_references', new=AsyncMock(return_value=1)),
):
self.db.reset_mock()
with self.assertRaises(ServiceWarning):
await getattr(MasterService, f'remove_{resource}')(self.db, 1)
self.db.rollback.assert_awaited_once()
self.db.commit.assert_not_awaited()
async def test_unreferenced_existing_records_are_deleted_for_all_resources(self) -> None:
for resource in RESOURCE_IDS:
with self.subTest(resource=resource), ExitStack() as stack:
self.db.reset_mock()
count_mock = stack.enter_context(
patch.object(MasterDao, 'count_references', new=AsyncMock(return_value=0))
)
stack.enter_context(
patch.object(MasterDao, f'get_{resource}', new=AsyncMock(return_value=SimpleNamespace()))
)
delete_mock = stack.enter_context(patch.object(MasterDao, f'delete_{resource}', new=AsyncMock()))
result = await getattr(MasterService, f'remove_{resource}')(self.db, 1)
self.assertTrue(result.is_success)
count_mock.assert_awaited_once_with(self.db, resource, 1)
delete_mock.assert_awaited_once_with(self.db, 1)
self.db.commit.assert_awaited_once()
self.db.rollback.assert_not_awaited()
async def test_remove_missing_record_is_explicit_for_all_resources(self) -> None:
for resource in RESOURCE_IDS:
with self.subTest(resource=resource), ExitStack() as stack:
stack.enter_context(patch.object(MasterDao, 'count_references', new=AsyncMock(return_value=0)))
stack.enter_context(patch.object(MasterDao, f'get_{resource}', new=AsyncMock(return_value=None)))
with self.assertRaises(ServiceException):
await getattr(MasterService, f'remove_{resource}')(self.db, 1)
async def test_batch_remove_failure_rolls_back_without_delete_or_commit(self) -> None:
delete_model = DeleteStrainModel(strainIds=[1, 2])
with (
patch.object(MasterDao, 'count_references', new=AsyncMock(side_effect=[0, 1])),
patch.object(MasterDao, 'get_strain', new=AsyncMock(return_value=SimpleNamespace())),
patch.object(MasterDao, 'delete_strain', new=AsyncMock()) as delete_mock,
self.assertRaises(ServiceWarning),
):
await MasterService.remove_strain(self.db, delete_model)
delete_mock.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_add_ignores_client_primary_key_before_calling_dao(self) -> None:
model = make_models(77)['strain']
with (
patch.object(MasterDao, 'find_strain_by_code', new=AsyncMock(return_value=None)),
patch.object(MasterDao, 'add_strain', new=AsyncMock()) as add_mock,
):
await MasterService.add_strain(self.db, model)
persisted_model = add_mock.await_args.args[1]
self.assertIsNone(persisted_model.strain_id)
async def test_integrity_errors_from_add_update_delete_and_commit_are_stable(self) -> None:
for phase in ('add', 'update', 'delete', 'commit'):
db = MagicMock(spec=AsyncSession)
with self.subTest(phase=phase), ExitStack() as stack:
if phase in {'add', 'commit'}:
stack.enter_context(
patch.object(MasterDao, 'find_strain_by_code', new=AsyncMock(return_value=None))
)
stack.enter_context(
patch.object(
MasterDao,
'add_strain',
new=AsyncMock(side_effect=integrity_error() if phase == 'add' else None),
)
)
if phase == 'commit':
db.commit.side_effect = integrity_error()
call = MasterService.add_strain(db, make_models()['strain'])
elif phase == 'update':
stack.enter_context(
patch.object(
MasterDao,
'get_strain',
new=AsyncMock(return_value=SimpleNamespace(strain_id=7)),
)
)
stack.enter_context(
patch.object(MasterDao, 'find_strain_by_code', new=AsyncMock(return_value=None))
)
stack.enter_context(
patch.object(MasterDao, 'update_strain', new=AsyncMock(side_effect=integrity_error()))
)
call = MasterService.edit_strain(db, make_models(7)['strain'])
else:
stack.enter_context(patch.object(MasterDao, 'count_references', new=AsyncMock(return_value=0)))
stack.enter_context(
patch.object(MasterDao, 'get_strain', new=AsyncMock(return_value=SimpleNamespace()))
)
stack.enter_context(
patch.object(MasterDao, 'delete_strain', new=AsyncMock(side_effect=integrity_error()))
)
call = MasterService.remove_strain(db, 7)
with self.assertRaises(ServiceException) as raised:
await call
self.assertIsNotNone(raised.exception.message)
self.assertNotIn('duplicate', raised.exception.message)
db.rollback.assert_awaited_once()
if phase != 'commit':
db.commit.assert_not_awaited()
async def test_non_integrity_exception_rolls_back_and_is_reraised(self) -> None:
error = RuntimeError('unexpected')
with (
patch.object(MasterDao, 'find_strain_by_code', new=AsyncMock(return_value=None)),
patch.object(MasterDao, 'add_strain', new=AsyncMock(side_effect=error)),
self.assertRaises(RuntimeError) as raised,
):
await MasterService.add_strain(self.db, make_models()['strain'])
self.assertIs(error, raised.exception)
self.db.rollback.assert_awaited_once()
self.db.commit.assert_not_awaited()
class TestMasterDao(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
self.db = MagicMock(spec=AsyncSession)
async def test_list_queries_cover_vo_filters_and_stable_primary_key_order(self) -> None:
cases = (
(
'strain',
StrainPageQueryModel(strainCode='S001', strainName='菌种', status='0', pageNum=2, pageSize=5),
('biz_strain.strain_code LIKE', 'biz_strain.strain_name LIKE', 'biz_strain.status =', 'strain_id'),
),
(
'customer',
CustomerPageQueryModel(customerCode='C001', customerName='客户', status='0', pageNum=2, pageSize=5),
(
'biz_customer.customer_code LIKE',
'biz_customer.customer_name LIKE',
'biz_customer.status =',
'customer_id',
),
),
(
'site',
SitePageQueryModel(
customerId=11,
siteCode='SITE001',
siteName='现场',
status='0',
pageNum=2,
pageSize=5,
),
(
'biz_customer_site.customer_id = 11',
'biz_customer_site.site_code LIKE',
'biz_customer_site.site_name LIKE',
'biz_customer_site.status =',
'site_id',
),
),
(
'line',
LinePageQueryModel(siteId=22, lineCode='L001', lineName='产线', status='0', pageNum=2, pageSize=5),
(
'biz_production_line.site_id = 22',
'biz_production_line.line_code LIKE',
'biz_production_line.line_name LIKE',
'biz_production_line.status =',
'line_id',
),
),
(
'warehouse',
WarehousePageQueryModel(warehouseCode='W001', warehouseName='仓库', status='0', pageNum=2, pageSize=5),
(
'biz_warehouse.warehouse_code LIKE',
'biz_warehouse.warehouse_name LIKE',
'biz_warehouse.status =',
'warehouse_id',
),
),
)
page = PageModel(rows=[], pageNum=2, pageSize=5, total=0, hasNext=False)
for resource, query_model, expected_tokens in cases:
with (
self.subTest(resource=resource),
patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=page)) as paginate_mock,
):
result = await getattr(MasterDao, f'list_{resource}')(self.db, query_model, is_page=True)
statement = paginate_mock.await_args.args[1]
sql = compile_sql(statement)
self.assertIs(page, result)
for token in expected_tokens[:-1]:
self.assertIn(token, sql)
self.assertIn(f'ORDER BY {statement.get_final_froms()[0].name}.{expected_tokens[-1]}', sql)
self.assertEqual((2, 5, True), paginate_mock.await_args.args[2:])
async def test_get_queries_use_expected_table_and_primary_key(self) -> None:
cases = (
('strain', 'biz_strain', 'strain_id'),
('customer', 'biz_customer', 'customer_id'),
('site', 'biz_customer_site', 'site_id'),
('line', 'biz_production_line', 'line_id'),
('warehouse', 'biz_warehouse', 'warehouse_id'),
)
for resource, table, id_field in cases:
result = MagicMock()
result.scalar_one_or_none.return_value = None
self.db.execute = AsyncMock(return_value=result)
await getattr(MasterDao, f'get_{resource}')(self.db, 9)
sql = compile_sql(self.db.execute.await_args.args[0])
self.assertIn(f'FROM {table}', sql)
self.assertIn(f'{table}.{id_field} = 9', sql)
async def test_unique_queries_use_global_or_parent_scoped_predicates(self) -> None:
cases = (
('strain', ('S001',), 'biz_strain', ('strain_code',)),
('customer', ('C001',), 'biz_customer', ('customer_code',)),
('warehouse', ('W001',), 'biz_warehouse', ('warehouse_code',)),
('site', ('SITE001', 11), 'biz_customer_site', ('site_code', 'customer_id')),
('line', ('L001', 22), 'biz_production_line', ('line_code', 'site_id')),
)
for resource, args, table, predicates in cases:
result = MagicMock()
result.scalar_one_or_none.return_value = None
self.db.execute = AsyncMock(return_value=result)
await getattr(MasterDao, f'find_{resource}_by_code')(self.db, *args)
sql = compile_sql(self.db.execute.await_args.args[0])
self.assertIn(f'FROM {table}', sql)
for predicate in predicates:
self.assertIn(f'{table}.{predicate} =', sql)
async def test_add_ignores_client_primary_keys_flushes_and_never_commits(self) -> None:
expected_classes = {
'strain': BizStrain,
'customer': BizCustomer,
'site': BizCustomerSite,
'line': BizProductionLine,
'warehouse': BizWarehouse,
}
for resource, model in make_models(77).items():
with self.subTest(resource=resource):
self.db.reset_mock()
record = await getattr(MasterDao, f'add_{resource}')(self.db, model)
self.assertIsInstance(record, expected_classes[resource])
self.assertIsNone(getattr(record, RESOURCE_IDS[resource]))
self.db.add.assert_called_once_with(record)
self.db.flush.assert_awaited_once()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_not_awaited()
async def test_update_and_delete_statements_use_primary_key_and_preserve_creation_audit(self) -> None:
table_names = {
'strain': 'biz_strain',
'customer': 'biz_customer',
'site': 'biz_customer_site',
'line': 'biz_production_line',
'warehouse': 'biz_warehouse',
}
created_at = datetime(2026, 1, 2, 3, 4, 5)
for resource, model in make_models(7).items():
model.create_by = '客户端伪造创建人'
model.create_time = created_at
model.update_by = 'server-user'
with self.subTest(resource=resource):
self.db.reset_mock()
await getattr(MasterDao, f'update_{resource}')(self.db, model)
update_sql = compile_sql(self.db.execute.await_args.args[0])
table = table_names[resource]
id_field = RESOURCE_IDS[resource]
self.assertIn(f'UPDATE {table} SET', update_sql)
self.assertIn(f'WHERE {table}.{id_field} = 7', update_sql)
self.assertNotIn('create_by', update_sql)
self.assertNotIn('create_time', update_sql)
self.db.commit.assert_not_awaited()
self.db.reset_mock()
await getattr(MasterDao, f'delete_{resource}')(self.db, 7)
delete_sql = compile_sql(self.db.execute.await_args.args[0])
self.assertIn(f'DELETE FROM {table}', delete_sql)
self.assertIn(f'{table}.{id_field} = 7', delete_sql)
self.db.commit.assert_not_awaited()
async def test_count_references_covers_every_direct_foreign_key(self) -> None:
expected = {
'strain': (('biz_filling_record', ('strain_id',)),),
'customer': (('biz_customer_site', ('customer_id',)),),
'site': (
('biz_production_line', ('site_id',)),
('biz_user_site', ('site_id',)),
('biz_shipment_order', ('target_site_id',)),
('biz_usage_record', ('site_id',)),
('biz_return_order', ('source_site_id',)),
('biz_pallet_inventory', ('site_id',)),
('biz_pallet_event', ('before_site_id', 'after_site_id')),
),
'line': (
('biz_usage_record', ('line_id',)),
('biz_pallet_inventory', ('current_line_id',)),
('biz_pallet_event', ('before_line_id', 'after_line_id')),
),
'warehouse': (
('biz_user_warehouse', ('warehouse_id',)),
('biz_inbound_order', ('warehouse_id',)),
('biz_filling_record', ('warehouse_id',)),
('biz_shipment_order', ('source_warehouse_id',)),
('biz_return_order', ('target_warehouse_id',)),
('biz_pallet_inventory', ('warehouse_id',)),
('biz_pallet_event', ('before_warehouse_id', 'after_warehouse_id')),
),
}
for resource, references in expected.items():
results = []
for _ in references:
result = MagicMock()
result.scalar.return_value = 0
results.append(result)
self.db.execute = AsyncMock(side_effect=results)
count = await MasterDao.count_references(self.db, resource, 9)
self.assertEqual(0, count)
self.assertEqual(len(references), self.db.execute.await_count)
for call, (table, columns) in zip(self.db.execute.await_args_list, references, strict=True):
sql = compile_sql(call.args[0])
self.assertIn(f'FROM {table}', sql)
for column in columns:
self.assertIn(f'{table}.{column} = 9', sql)
self.db.commit.assert_not_awaited()
class TestMasterControllers(unittest.TestCase):
ROUTERS = {
'strain': strain_controller_module.strain_controller,
'customer': customer_controller_module.customer_controller,
'site': site_controller_module.site_controller,
'line': line_controller_module.line_controller,
'warehouse': warehouse_controller_module.warehouse_controller,
}
MODULES = {
'strain': strain_controller_module,
'customer': customer_controller_module,
'site': site_controller_module,
'line': line_controller_module,
'warehouse': warehouse_controller_module,
}
def test_openapi_contains_all_resource_paths_methods_and_response_models(self) -> None:
app = FastAPI()
for router in self.ROUTERS.values():
app.include_router(router)
schema = app.openapi()
for resource in self.ROUTERS:
prefix = f'/master/{resource}'
self.assertIn(f'{prefix}/list', schema['paths'])
self.assertIn('get', schema['paths'][f'{prefix}/list'])
self.assertIn(prefix, schema['paths'])
self.assertIn('post', schema['paths'][prefix])
self.assertIn('put', schema['paths'][prefix])
self.assertIn(f'{prefix}/{{{resource}_ids}}', schema['paths'])
self.assertIn('delete', schema['paths'][f'{prefix}/{{{resource}_ids}}'])
self.assertIn(f'{prefix}/{{{resource}_id}}', schema['paths'])
self.assertIn('get', schema['paths'][f'{prefix}/{{{resource}_id}}'])
def test_all_twenty_five_permission_dependencies_are_exact(self) -> None:
expected_permissions = {
(f'/master/{resource}/list', 'GET'): f'master:{resource}:list' for resource in self.ROUTERS
}
for resource in self.ROUTERS:
expected_permissions.update(
{
(f'/master/{resource}/{{{resource}_id}}', 'GET'): f'master:{resource}:query',
(f'/master/{resource}', 'POST'): f'master:{resource}:add',
(f'/master/{resource}', 'PUT'): f'master:{resource}:edit',
(f'/master/{resource}/{{{resource}_ids}}', 'DELETE'): f'master:{resource}:remove',
}
)
actual_permissions = {}
for router in self.ROUTERS.values():
for route in router.routes:
if not isinstance(route, APIRoute):
continue
method = next(iter(route.methods))
auth_dependencies = [
dependency.dependency
for dependency in route.dependencies
if isinstance(dependency.dependency, CheckUserInterfaceAuth)
]
self.assertEqual(1, len(auth_dependencies))
self.assertTrue(any(isinstance(dependency.dependency, PreAuth) for dependency in route.dependencies))
actual_permissions[(route.path, method)] = auth_dependencies[0].perm
self.assertEqual(expected_permissions, actual_permissions)
self.assertEqual(25, len(actual_permissions))
def test_write_routes_have_log_and_current_user_dependency(self) -> None:
for router in self.ROUTERS.values():
for route in router.routes:
if not isinstance(route, APIRoute) or route.methods.isdisjoint({'POST', 'PUT', 'DELETE'}):
continue
self.assertTrue(hasattr(route.endpoint, '__wrapped__'))
self.assertTrue(
any(
dependency.name == 'current_user' and dependency.call == LoginService.get_current_user
for dependency in route.dependant.dependencies
)
)
def test_controller_source_has_three_log_and_current_user_declarations(self) -> None:
for resource, module in self.MODULES.items():
with self.subTest(resource=resource):
source = inspect.getsource(module)
self.assertEqual(3, source.count('@Log('))
self.assertEqual(3, source.count('CurrentUserDependency()'))
self.assertNotIn('ApiCache', source)
def test_route_response_model_contracts(self) -> None:
model_by_resource = {
'strain': StrainModel,
'customer': CustomerModel,
'site': SiteModel,
'line': LineModel,
'warehouse': WarehouseModel,
}
for resource, router in self.ROUTERS.items():
for route in router.routes:
if not isinstance(route, APIRoute):
continue
if route.path.endswith('/list'):
self.assertEqual(PageResponseModel[model_by_resource[resource]], route.response_model)
elif route.methods == {'GET'}:
self.assertEqual(DataResponseModel[model_by_resource[resource]], route.response_model)
else:
self.assertEqual('ResponseBaseModel', route.response_model.__name__)
class TestDeleteModelCoverage(unittest.TestCase):
def test_all_delete_models_are_used_by_service_contract(self) -> None:
models = (
DeleteStrainModel(strainIds=[1]),
DeleteCustomerModel(customerIds=[1]),
DeleteSiteModel(siteIds=[1]),
DeleteLineModel(lineIds=[1]),
DeleteWarehouseModel(warehouseIds=[1]),
)
self.assertEqual(5, len(models))
self.assertTrue(all(next(iter(model.model_dump().values())) == [1] for model in models))
if __name__ == '__main__':
unittest.main()