功能:新增业务基础资料接口
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user