补充主数据删除能力契约

This commit is contained in:
焦龙言
2026-07-27 04:34:10 +08:00
parent d1f0b9d48b
commit 33f29df40e
6 changed files with 311 additions and 12 deletions
@@ -78,6 +78,14 @@ _REFERENCE_COLUMNS: dict[MasterResource, tuple[tuple[InstrumentedAttribute, ...]
),
}
_MASTER_ID_COLUMNS: dict[MasterResource, InstrumentedAttribute] = {
'strain': BizStrain.strain_id,
'customer': BizCustomer.customer_id,
'site': BizCustomerSite.site_id,
'line': BizProductionLine.line_id,
'warehouse': BizWarehouse.warehouse_id,
}
_PALLET_REMOVAL_BLOCKING_COLUMNS: tuple[InstrumentedAttribute, ...] = (
BizInboundItem.pallet_id,
BizFillingRecord.pallet_id,
@@ -139,7 +147,7 @@ class MasterDao:
id_field: str,
page_object: BaseModel,
) -> Any:
payload = page_object.model_dump(exclude={id_field})
payload = page_object.model_dump(exclude={id_field, 'can_remove'})
record = model(**payload)
db.add(record)
await db.flush()
@@ -155,7 +163,7 @@ class MasterDao:
) -> None:
payload = page_object.model_dump(
exclude_unset=True,
exclude={'create_by', 'create_time'},
exclude={'can_remove', 'create_by', 'create_time'},
)
resource_id = payload.pop(id_field)
await db.execute(update(model).where(id_column == resource_id).values(**payload))
@@ -169,6 +177,38 @@ class MasterDao:
) -> None:
await db.execute(delete(model).where(id_column == resource_id))
@classmethod
async def get_master_removability(
cls,
db: AsyncSession,
resource_type: MasterResource,
resource_ids: list[int],
) -> dict[int, bool]:
"""按删除约束批量判断五类主数据是否允许删除。"""
removability = dict.fromkeys(resource_ids, False)
if not resource_ids:
return removability
id_column = _MASTER_ID_COLUMNS.get(resource_type)
references = _REFERENCE_COLUMNS.get(resource_type)
if id_column is None or references is None:
raise ValueError(f'不支持的主数据类型:{resource_type}')
has_no_references = []
for columns in references:
predicates = [column == id_column for column in columns]
predicate = predicates[0] if len(predicates) == 1 else or_(*predicates)
has_no_references.append(~select(columns[0]).where(predicate).exists())
statement = select(
id_column,
and_(*has_no_references).label('can_remove'),
).where(id_column.in_(resource_ids))
result = await db.execute(statement)
for resource_id, can_remove in result.all():
removability[resource_id] = bool(can_remove)
return removability
@classmethod
async def list_strain(
cls,
@@ -32,6 +32,11 @@ class StrainModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class StrainPageQueryModel(BaseModel):
@@ -86,6 +91,11 @@ class CustomerModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class CustomerPageQueryModel(BaseModel):
@@ -148,6 +158,11 @@ class SiteModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class SitePageQueryModel(BaseModel):
@@ -204,6 +219,11 @@ class LineModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class LinePageQueryModel(BaseModel):
@@ -260,6 +280,11 @@ class WarehouseModel(BaseModel):
update_by: str | None = Field(default=None, max_length=64, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, max_length=500, description='备注')
can_remove: bool | None = Field(
default=None,
description='是否允许删除',
json_schema_extra={'readOnly': True},
)
class WarehousePageQueryModel(BaseModel):
@@ -386,7 +411,7 @@ class UserAssignModel(BaseModel):
extra='forbid',
)
user_ids: list[PositiveId] = Field(min_length=1, description='用户ID列表')
user_ids: list[PositiveId] = Field(description='用户ID列表')
_validate_unique_ids = field_validator('user_ids')(_ensure_unique_ids)
@@ -143,8 +143,24 @@ class MasterService:
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)
) -> 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:
@@ -152,7 +168,9 @@ class MasterService:
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)
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(
@@ -235,7 +253,7 @@ class MasterService:
db: AsyncSession,
query_object: StrainPageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
) -> PageModel | list[StrainModel]:
return await cls._list(db, 'strain', query_object, is_page)
@classmethod
@@ -264,7 +282,7 @@ class MasterService:
db: AsyncSession,
query_object: CustomerPageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
) -> PageModel | list[CustomerModel]:
return await cls._list(db, 'customer', query_object, is_page)
@classmethod
@@ -293,7 +311,7 @@ class MasterService:
db: AsyncSession,
query_object: SitePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
) -> PageModel | list[SiteModel]:
return await cls._list(db, 'site', query_object, is_page)
@classmethod
@@ -322,7 +340,7 @@ class MasterService:
db: AsyncSession,
query_object: LinePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
) -> PageModel | list[LineModel]:
return await cls._list(db, 'line', query_object, is_page)
@classmethod
@@ -351,7 +369,7 @@ class MasterService:
db: AsyncSession,
query_object: WarehousePageQueryModel,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
) -> PageModel | list[WarehouseModel]:
return await cls._list(db, 'warehouse', query_object, is_page)
@classmethod