Files
ShiErFeng/shierfeng-fastapi-backend/module_business/service/master_service.py
T

394 lines
14 KiB
Python

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[BaseModel]:
result = await getattr(MasterDao, f'list_{resource}')(db, query_object, is_page)
config = _RESOURCE_CONFIG[resource]
records = result.rows if isinstance(result, PageModel) else result
models = [config['vo'].model_validate(record) for record in records]
id_field = config['id_field']
removability = await MasterDao.get_master_removability(
db,
resource,
[getattr(model, id_field) for model in models],
)
rows = [
model.model_copy(update={'can_remove': removability.get(getattr(model, id_field), False)})
for model in models
]
if isinstance(result, PageModel):
return result.model_copy(update={'rows': rows})
return rows
@classmethod
async def _detail(cls, db: AsyncSession, resource: MasterResource, resource_id: int) -> BaseModel:
config = _RESOURCE_CONFIG[resource]
record = await getattr(MasterDao, f'get_{resource}')(db, resource_id)
if record is None:
raise ServiceException(message=f'{config["label"]}不存在')
model = config['vo'].model_validate(record)
removability = await MasterDao.get_master_removability(db, resource, [resource_id])
return model.model_copy(update={'can_remove': removability.get(resource_id, False)})
@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[StrainModel]:
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[CustomerModel]:
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[SiteModel]:
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[LineModel]:
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[WarehouseModel]:
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)