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

This commit is contained in:
焦龙言
2026-09-22 18:16:30 +08:00
parent af5428d0e3
commit b6f606d9f2
13 changed files with 1382 additions and 1131 deletions
@@ -1,4 +1,4 @@
from typing import Any, Literal
from typing import Any, Literal, NoReturn
from pydantic import BaseModel
from sqlalchemy.exc import IntegrityError
@@ -6,7 +6,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dao.master_dao import MasterDao
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
from module_business.entity.vo.master_vo import (
CustomerModel,
CustomerPageQueryModel,
@@ -24,6 +26,7 @@ from module_business.entity.vo.master_vo import (
WarehouseModel,
WarehousePageQueryModel,
)
from module_business.service.test_batch_access_service import TestBatchAccessService
MasterResource = Literal['strain', 'customer', 'site', 'line', 'warehouse']
@@ -71,17 +74,35 @@ class MasterService:
"""菌种、客户、现场、产线和仓库主数据服务层。"""
@staticmethod
async def _validate_parent(db: AsyncSession, resource: MasterResource, page_object: BaseModel) -> None:
async def _prepare_test_write(
db: AsyncSession,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> None:
if scope.mode != 'TEST':
return
TestBatchAccessService.assert_super_admin(current_user)
await TestBatchAccessService.lock_active_batch(db, scope)
@staticmethod
async def _validate_parent(
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
scope: TestBatchScope,
) -> None:
if resource == 'site':
customer = await MasterDao.get_customer(db, page_object.customer_id)
customer = await MasterDao.get_customer(db, page_object.customer_id, scope)
if customer is None:
raise ServiceException(message='目标客户不存在')
assert_record_in_scope(customer, scope, '目标客户')
if customer.status != '0':
raise ServiceException(message='目标客户已停用')
elif resource == 'line':
site = await MasterDao.get_site(db, page_object.site_id)
site = await MasterDao.get_site(db, page_object.site_id, scope)
if site is None:
raise ServiceException(message='目标客户现场不存在')
assert_record_in_scope(site, scope, '目标客户现场')
if site.status != '0':
raise ServiceException(message='目标客户现场已停用')
@@ -127,50 +148,37 @@ class MasterService:
code = getattr(page_object, config['code_field'])
raise ServiceException(message=f'{config["label"]}编码{code}已存在')
@staticmethod
def _extract_ids(resource: MasterResource, value: int | BaseModel) -> list[int]:
ids = [value] if isinstance(value, int) else list(getattr(value, _RESOURCE_CONFIG[resource]['ids_field']))
if not ids or any(resource_id <= 0 for resource_id in ids):
raise ServiceException(message='主数据ID必须为正整数')
if len(ids) != len(set(ids)):
raise ServiceException(message='主数据ID不能重复')
return ids
@classmethod
async def _list(
cls,
db: AsyncSession,
resource: MasterResource,
query_object: BaseModel,
scope: TestBatchScope,
is_page: bool,
) -> PageModel | list[BaseModel]:
result = await getattr(MasterDao, f'list_{resource}')(db, query_object, is_page)
result = await getattr(MasterDao, f'list_{resource}')(db, query_object, scope, is_page)
config = _RESOURCE_CONFIG[resource]
records = result.rows if isinstance(result, PageModel) else result
models = [config['vo'].model_validate(record) for record in records]
id_field = config['id_field']
removability = await MasterDao.get_master_removability(
db,
resource,
[getattr(model, id_field) for model in models],
)
rows = [
model.model_copy(update={'can_remove': removability.get(getattr(model, id_field), False)})
for model in models
]
rows = [config['vo'].model_validate(record) for record in records]
if isinstance(result, PageModel):
return result.model_copy(update={'rows': rows})
return rows
@classmethod
async def _detail(cls, db: AsyncSession, resource: MasterResource, resource_id: int) -> BaseModel:
async def _detail(
cls,
db: AsyncSession,
resource: MasterResource,
resource_id: int,
scope: TestBatchScope,
) -> BaseModel:
config = _RESOURCE_CONFIG[resource]
record = await getattr(MasterDao, f'get_{resource}')(db, resource_id)
record = await getattr(MasterDao, f'get_{resource}')(db, resource_id, scope)
if record is None:
raise ServiceException(message=f'{config["label"]}不存在')
model = config['vo'].model_validate(record)
removability = await MasterDao.get_master_removability(db, resource, [resource_id])
return model.model_copy(update={'can_remove': removability.get(resource_id, False)})
assert_record_in_scope(record, scope, config['label'])
return config['vo'].model_validate(record)
@classmethod
async def _add(
@@ -178,13 +186,16 @@ class MasterService:
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
config = _RESOURCE_CONFIG[resource]
try:
await cls._validate_parent(db, resource, page_object)
await cls._prepare_test_write(db, current_user, scope)
await cls._validate_parent(db, resource, page_object, scope)
await cls._validate_unique(db, resource, page_object)
safe_object = page_object.model_copy(update={config['id_field']: None})
await getattr(MasterDao, f'add_{resource}')(db, safe_object)
await getattr(MasterDao, f'add_{resource}')(db, safe_object, scope.batch_id)
await db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except IntegrityError:
@@ -200,18 +211,27 @@ class MasterService:
db: AsyncSession,
resource: MasterResource,
page_object: BaseModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
config = _RESOURCE_CONFIG[resource]
try:
await cls._prepare_test_write(db, current_user, scope)
resource_id = getattr(page_object, config['id_field'])
if resource_id is None:
raise ServiceException(message=f'{config["label"]}ID不能为空')
current = await getattr(MasterDao, f'get_{resource}')(db, resource_id)
current = await getattr(MasterDao, f'get_{resource}')(
db,
resource_id,
scope,
for_update=scope.mode == 'TEST',
)
if current is None:
raise ServiceException(message=f'{config["label"]}不存在')
await cls._validate_parent(db, resource, page_object)
assert_record_in_scope(current, scope, config['label'])
await cls._validate_parent(db, resource, page_object, scope)
await cls._validate_unique(db, resource, page_object, resource_id)
await getattr(MasterDao, f'update_{resource}')(db, page_object)
await getattr(MasterDao, f'update_{resource}')(db, page_object, scope)
await db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except IntegrityError:
@@ -221,173 +241,216 @@ class MasterService:
await db.rollback()
raise
@classmethod
async def _remove(
cls,
db: AsyncSession,
resource: MasterResource,
value: int | BaseModel,
) -> CrudResponseModel:
config = _RESOURCE_CONFIG[resource]
try:
resource_ids = cls._extract_ids(resource, value)
for resource_id in resource_ids:
if await MasterDao.count_references(db, resource, resource_id) > 0:
raise ServiceWarning(message=f'{config["label"]}已被引用,只能停用,不能删除')
if await getattr(MasterDao, f'get_{resource}')(db, resource_id) is None:
raise ServiceException(message=f'{config["label"]}不存在')
for resource_id in resource_ids:
await getattr(MasterDao, f'delete_{resource}')(db, resource_id)
await db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except IntegrityError:
await db.rollback()
raise ServiceException(message=f'删除{config["label"]}失败,该数据仍被引用') from None
except Exception:
await db.rollback()
raise
@staticmethod
async def reject_physical_delete() -> NoReturn:
raise ServiceWarning('', '不支持单条物理删除,请通过测试数据批次清理')
@classmethod
async def list_strain(
cls,
db: AsyncSession,
query_object: StrainPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[StrainModel]:
return await cls._list(db, 'strain', query_object, is_page)
return await cls._list(db, 'strain', query_object, scope, is_page)
@classmethod
async def detail_strain(cls, db: AsyncSession, strain_id: int) -> StrainModel:
return await cls._detail(db, 'strain', strain_id)
async def detail_strain(cls, db: AsyncSession, strain_id: int, scope: TestBatchScope) -> StrainModel:
return await cls._detail(db, 'strain', strain_id, scope)
@classmethod
async def add_strain(cls, db: AsyncSession, strain: StrainModel) -> CrudResponseModel:
return await cls._add(db, 'strain', strain)
async def add_strain(
cls,
db: AsyncSession,
strain: StrainModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'strain', strain, current_user, scope)
@classmethod
async def edit_strain(cls, db: AsyncSession, strain: StrainModel) -> CrudResponseModel:
return await cls._edit(db, 'strain', strain)
async def edit_strain(
cls,
db: AsyncSession,
strain: StrainModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'strain', strain, current_user, scope)
@classmethod
async def remove_strain(
cls,
db: AsyncSession,
value: int | DeleteStrainModel,
) -> CrudResponseModel:
return await cls._remove(db, 'strain', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@classmethod
async def list_customer(
cls,
db: AsyncSession,
query_object: CustomerPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[CustomerModel]:
return await cls._list(db, 'customer', query_object, is_page)
return await cls._list(db, 'customer', query_object, scope, is_page)
@classmethod
async def detail_customer(cls, db: AsyncSession, customer_id: int) -> CustomerModel:
return await cls._detail(db, 'customer', customer_id)
async def detail_customer(cls, db: AsyncSession, customer_id: int, scope: TestBatchScope) -> CustomerModel:
return await cls._detail(db, 'customer', customer_id, scope)
@classmethod
async def add_customer(cls, db: AsyncSession, customer: CustomerModel) -> CrudResponseModel:
return await cls._add(db, 'customer', customer)
async def add_customer(
cls,
db: AsyncSession,
customer: CustomerModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'customer', customer, current_user, scope)
@classmethod
async def edit_customer(cls, db: AsyncSession, customer: CustomerModel) -> CrudResponseModel:
return await cls._edit(db, 'customer', customer)
async def edit_customer(
cls,
db: AsyncSession,
customer: CustomerModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'customer', customer, current_user, scope)
@classmethod
async def remove_customer(
cls,
db: AsyncSession,
value: int | DeleteCustomerModel,
) -> CrudResponseModel:
return await cls._remove(db, 'customer', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@classmethod
async def list_site(
cls,
db: AsyncSession,
query_object: SitePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[SiteModel]:
return await cls._list(db, 'site', query_object, is_page)
return await cls._list(db, 'site', query_object, scope, is_page)
@classmethod
async def detail_site(cls, db: AsyncSession, site_id: int) -> SiteModel:
return await cls._detail(db, 'site', site_id)
async def detail_site(cls, db: AsyncSession, site_id: int, scope: TestBatchScope) -> SiteModel:
return await cls._detail(db, 'site', site_id, scope)
@classmethod
async def add_site(cls, db: AsyncSession, site: SiteModel) -> CrudResponseModel:
return await cls._add(db, 'site', site)
async def add_site(
cls,
db: AsyncSession,
site: SiteModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'site', site, current_user, scope)
@classmethod
async def edit_site(cls, db: AsyncSession, site: SiteModel) -> CrudResponseModel:
return await cls._edit(db, 'site', site)
async def edit_site(
cls,
db: AsyncSession,
site: SiteModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'site', site, current_user, scope)
@classmethod
async def remove_site(
cls,
db: AsyncSession,
value: int | DeleteSiteModel,
) -> CrudResponseModel:
return await cls._remove(db, 'site', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@classmethod
async def list_line(
cls,
db: AsyncSession,
query_object: LinePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[LineModel]:
return await cls._list(db, 'line', query_object, is_page)
return await cls._list(db, 'line', query_object, scope, is_page)
@classmethod
async def detail_line(cls, db: AsyncSession, line_id: int) -> LineModel:
return await cls._detail(db, 'line', line_id)
async def detail_line(cls, db: AsyncSession, line_id: int, scope: TestBatchScope) -> LineModel:
return await cls._detail(db, 'line', line_id, scope)
@classmethod
async def add_line(cls, db: AsyncSession, line: LineModel) -> CrudResponseModel:
return await cls._add(db, 'line', line)
async def add_line(
cls,
db: AsyncSession,
line: LineModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'line', line, current_user, scope)
@classmethod
async def edit_line(cls, db: AsyncSession, line: LineModel) -> CrudResponseModel:
return await cls._edit(db, 'line', line)
async def edit_line(
cls,
db: AsyncSession,
line: LineModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'line', line, current_user, scope)
@classmethod
async def remove_line(
cls,
db: AsyncSession,
value: int | DeleteLineModel,
) -> CrudResponseModel:
return await cls._remove(db, 'line', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@classmethod
async def list_warehouse(
cls,
db: AsyncSession,
query_object: WarehousePageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[WarehouseModel]:
return await cls._list(db, 'warehouse', query_object, is_page)
return await cls._list(db, 'warehouse', query_object, scope, is_page)
@classmethod
async def detail_warehouse(cls, db: AsyncSession, warehouse_id: int) -> WarehouseModel:
return await cls._detail(db, 'warehouse', warehouse_id)
async def detail_warehouse(cls, db: AsyncSession, warehouse_id: int, scope: TestBatchScope) -> WarehouseModel:
return await cls._detail(db, 'warehouse', warehouse_id, scope)
@classmethod
async def add_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel) -> CrudResponseModel:
return await cls._add(db, 'warehouse', warehouse)
async def add_warehouse(
cls,
db: AsyncSession,
warehouse: WarehouseModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._add(db, 'warehouse', warehouse, current_user, scope)
@classmethod
async def edit_warehouse(cls, db: AsyncSession, warehouse: WarehouseModel) -> CrudResponseModel:
return await cls._edit(db, 'warehouse', warehouse)
async def edit_warehouse(
cls,
db: AsyncSession,
warehouse: WarehouseModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._edit(db, 'warehouse', warehouse, current_user, scope)
@classmethod
async def remove_warehouse(
cls,
db: AsyncSession,
value: int | DeleteWarehouseModel,
) -> CrudResponseModel:
return await cls._remove(db, 'warehouse', value)
) -> NoReturn:
return await cls.reject_physical_delete()
@@ -1,16 +1,18 @@
import secrets
from datetime import datetime
from io import BytesIO
from typing import Literal
from typing import Literal, NoReturn
import qrcode
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from exceptions.exception import ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dao.access_dao import AccessDao
from module_business.dao.master_dao import MasterDao
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import BizPalletInventory
from module_business.entity.vo.master_vo import (
@@ -22,6 +24,7 @@ from module_business.entity.vo.master_vo import (
PalletPageQueryModel,
UserAssignModel,
)
from module_business.service.test_batch_access_service import TestBatchAccessService
AssignmentResource = Literal['warehouse', 'site']
@@ -41,33 +44,43 @@ class PalletService:
qrcode.make(qr_token).save(buffer, format='PNG')
return buffer.getvalue()
@staticmethod
async def _prepare_test_write(
db: AsyncSession,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> None:
if scope.mode != 'TEST':
return
TestBatchAccessService.assert_super_admin(current_user)
await TestBatchAccessService.lock_active_batch(db, scope)
@classmethod
async def get_pallet_qrcode(cls, db: AsyncSession, pallet_id: int) -> bytes:
pallet = await MasterDao.get_pallet(db, pallet_id)
async def get_pallet_qrcode(cls, db: AsyncSession, pallet_id: int, scope: TestBatchScope) -> bytes:
pallet = await MasterDao.get_pallet(db, pallet_id, scope)
if pallet is None:
raise ServiceException(message='托盘不存在')
assert_record_in_scope(pallet, scope, '托盘')
return cls.render_qr_png(pallet.qr_token)
@staticmethod
def _extract_pallet_ids(value: int | DeletePalletModel) -> list[int]:
pallet_ids = [value] if isinstance(value, int) else list(value.pallet_ids)
if not pallet_ids or any(pallet_id <= 0 for pallet_id in pallet_ids):
raise ServiceException(message='托盘ID必须为正整数')
if len(pallet_ids) != len(set(pallet_ids)):
raise ServiceException(message='托盘ID不能重复')
return sorted(pallet_ids)
@classmethod
async def _lock_pallet_inventory(cls, db: AsyncSession, pallet_id: int) -> tuple[BizPallet, BizPalletInventory]:
async def _lock_pallet_inventory(
cls,
db: AsyncSession,
pallet_id: int,
scope: TestBatchScope,
) -> tuple[BizPallet, BizPalletInventory]:
"""按库存、托盘顺序加锁,避免与流转事务形成反序死锁。"""
inventory = await MasterDao.get_pallet_inventory(db, pallet_id, for_update=True)
inventory = await MasterDao.get_pallet_inventory(db, pallet_id, scope, for_update=True)
if inventory is None:
if await MasterDao.get_pallet(db, pallet_id) is None:
if await MasterDao.get_pallet(db, pallet_id, scope) is None:
raise ServiceException(message='托盘不存在')
raise ServiceException(message='托盘库存不存在')
pallet = await MasterDao.get_pallet(db, pallet_id, for_update=True)
assert_record_in_scope(inventory, scope, '托盘库存')
pallet = await MasterDao.get_pallet(db, pallet_id, scope, for_update=True)
if pallet is None:
raise ServiceException(message='托盘不存在')
assert_record_in_scope(pallet, scope, '托盘')
return pallet, inventory
@classmethod
@@ -75,42 +88,41 @@ class PalletService:
cls,
db: AsyncSession,
query_object: PalletPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[PalletModel]:
result = await MasterDao.list_pallet(db, query_object, is_page)
result = await MasterDao.list_pallet(db, query_object, scope, is_page)
records = result.rows if isinstance(result, PageModel) else result
models = [PalletModel.model_validate(record) for record in records]
removability = await MasterDao.get_pallet_removability(
db,
[model.pallet_id for model in models],
)
rows = [model.model_copy(update={'can_remove': removability.get(model.pallet_id, False)}) for model in models]
rows = [PalletModel.model_validate(record) for record in records]
if isinstance(result, PageModel):
return result.model_copy(update={'rows': rows})
return rows
@classmethod
async def detail_pallet(cls, db: AsyncSession, pallet_id: int) -> PalletModel:
pallet = await MasterDao.get_pallet(db, pallet_id)
async def detail_pallet(cls, db: AsyncSession, pallet_id: int, scope: TestBatchScope) -> PalletModel:
pallet = await MasterDao.get_pallet(db, pallet_id, scope)
if pallet is None:
raise ServiceException(message='托盘不存在')
model = PalletModel.model_validate(pallet)
removability = await MasterDao.get_pallet_removability(db, [pallet_id])
return model.model_copy(update={'can_remove': removability.get(pallet_id, False)})
assert_record_in_scope(pallet, scope, '托盘')
return PalletModel.model_validate(pallet)
@classmethod
async def add_pallet(
cls,
db: AsyncSession,
pallet: PalletCreateModel,
current_user_name: str,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
try:
await cls._prepare_test_write(db, current_user, scope)
if await MasterDao.find_pallet_by_code(db, pallet.pallet_code) is not None:
raise ServiceException(message='托盘编号已存在')
current_time = datetime.now()
current_user_name = current_user.user.user_name
pallet_record = BizPallet(
test_batch_id=scope.batch_id,
pallet_code=pallet.pallet_code,
qr_token=secrets.token_urlsafe(32),
capacity_packages=pallet.capacity_packages,
@@ -124,6 +136,7 @@ class PalletService:
pallet_record = await MasterDao.add_pallet(db, pallet_record)
inventory = BizPalletInventory(
pallet_id=pallet_record.pallet_id,
test_batch_id=scope.batch_id,
flow_state='UNSTOCKED',
remaining_packages=0,
version=0,
@@ -144,16 +157,19 @@ class PalletService:
cls,
db: AsyncSession,
pallet: PalletEditModel,
current_user_name: str,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
try:
_, inventory = await cls._lock_pallet_inventory(db, pallet.pallet_id)
await cls._prepare_test_write(db, current_user, scope)
_, inventory = await cls._lock_pallet_inventory(db, pallet.pallet_id, scope)
if pallet.capacity_packages < inventory.remaining_packages:
raise ServiceException(message='托盘容量不能小于当前剩余包数')
await MasterDao.update_pallet(
db,
pallet,
update_by=current_user_name,
scope,
update_by=current_user.user.user_name,
update_time=datetime.now(),
)
await db.commit()
@@ -170,27 +186,8 @@ class PalletService:
cls,
db: AsyncSession,
value: int | DeletePalletModel,
) -> CrudResponseModel:
try:
pallet_ids = cls._extract_pallet_ids(value)
for pallet_id in pallet_ids:
await cls._lock_pallet_inventory(db, pallet_id)
removability = await MasterDao.get_pallet_removability(db, pallet_ids)
if not all(removability.get(pallet_id, False) for pallet_id in pallet_ids):
raise ServiceException(message='托盘状态或关联数据不允许删除')
for pallet_id in pallet_ids:
await MasterDao.delete_pallet_inventory(db, pallet_id)
await MasterDao.delete_pallet(db, pallet_id)
await db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except IntegrityError:
await db.rollback()
raise ServiceException(message='删除托盘失败,托盘状态或关联数据已变化') from None
except Exception:
await db.rollback()
raise
) -> NoReturn:
raise ServiceWarning('', '不支持单条物理删除,请通过测试数据批次清理')
@classmethod
async def _list_resource_users(
@@ -198,12 +195,17 @@ class PalletService:
db: AsyncSession,
resource: AssignmentResource,
resource_id: int,
scope: TestBatchScope,
) -> list[BusinessUserOptionModel]:
if await getattr(MasterDao, f'get_{resource}')(db, resource_id) is None:
record = await getattr(MasterDao, f'get_{resource}')(db, resource_id, scope)
if record is None:
raise ServiceException(message=f'{_ASSIGNMENT_LABELS[resource]}不存在')
assert_record_in_scope(record, scope, _ASSIGNMENT_LABELS[resource])
users = await MasterDao.list_assignable_users(db)
assigned_user_ids = set(await getattr(MasterDao, f'list_{resource}_assigned_user_ids')(db, resource_id))
assigned_user_ids = set(
await getattr(MasterDao, f'list_{resource}_assigned_user_ids')(db, resource_id, scope)
)
return [
BusinessUserOptionModel(
user_id=user.user_id,
@@ -219,16 +221,18 @@ class PalletService:
cls,
db: AsyncSession,
warehouse_id: int,
scope: TestBatchScope,
) -> list[BusinessUserOptionModel]:
return await cls._list_resource_users(db, 'warehouse', warehouse_id)
return await cls._list_resource_users(db, 'warehouse', warehouse_id, scope)
@classmethod
async def list_site_users(
cls,
db: AsyncSession,
site_id: int,
scope: TestBatchScope,
) -> list[BusinessUserOptionModel]:
return await cls._list_resource_users(db, 'site', site_id)
return await cls._list_resource_users(db, 'site', site_id, scope)
@classmethod
async def _assign_resource_users(
@@ -237,18 +241,21 @@ class PalletService:
resource: AssignmentResource,
resource_id: int,
assignment: UserAssignModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
label = _ASSIGNMENT_LABELS[resource]
try:
if (
await getattr(MasterDao, f'get_{resource}')(
db,
resource_id,
for_update=True,
)
is None
):
await cls._prepare_test_write(db, current_user, scope)
record = await getattr(MasterDao, f'get_{resource}')(
db,
resource_id,
scope,
for_update=True,
)
if record is None:
raise ServiceException(message=f'{label}不存在')
assert_record_in_scope(record, scope, label)
users = await MasterDao.list_assignable_users_by_ids(db, assignment.user_ids)
if {user.user_id for user in users} != set(assignment.user_ids):
raise ServiceException(message='存在不存在、停用或已删除的用户')
@@ -268,8 +275,17 @@ class PalletService:
db: AsyncSession,
warehouse_id: int,
assignment: UserAssignModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._assign_resource_users(db, 'warehouse', warehouse_id, assignment)
return await cls._assign_resource_users(
db,
'warehouse',
warehouse_id,
assignment,
current_user,
scope,
)
@classmethod
async def assign_site_users(
@@ -277,5 +293,14 @@ class PalletService:
db: AsyncSession,
site_id: int,
assignment: UserAssignModel,
current_user: CurrentUserModel,
scope: TestBatchScope,
) -> CrudResponseModel:
return await cls._assign_resource_users(db, 'site', site_id, assignment)
return await cls._assign_resource_users(
db,
'site',
site_id,
assignment,
current_user,
scope,
)