323 lines
12 KiB
Python
323 lines
12 KiB
Python
import json
|
|
import secrets
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from common.vo import CrudResponseModel
|
|
from exceptions.exception import PermissionException, ServiceException, ServiceWarning
|
|
from module_admin.entity.vo.user_vo import CurrentUserModel
|
|
from module_business.dao.test_batch_dao import TestBatchDao
|
|
from module_business.domain.enums import TestBatchStatus
|
|
from module_business.entity.do.test_batch_do import BizTestBatch
|
|
from module_business.entity.vo.test_batch_vo import (
|
|
BatchImpactModel,
|
|
TestBatchCreateModel,
|
|
TestBatchDetailModel,
|
|
TestBatchEditModel,
|
|
TestBatchModel,
|
|
TestBatchOptionModel,
|
|
TestBatchUserAssignModel,
|
|
)
|
|
from module_business.service.test_batch_access_service import TestBatchAccessService
|
|
|
|
_OWNED_TABLE_LABELS: tuple[tuple[str, str], ...] = (
|
|
('biz_strain', '菌种'),
|
|
('biz_customer', '客户'),
|
|
('biz_customer_site', '客户现场'),
|
|
('biz_production_line', '生产线'),
|
|
('biz_warehouse', '仓库'),
|
|
('biz_pallet', '托盘'),
|
|
('biz_inbound_order', '入库单'),
|
|
('biz_inbound_item', '入库明细'),
|
|
('biz_filling_record', '装填记录'),
|
|
('biz_shipment_order', '发货单'),
|
|
('biz_shipment_item', '发货明细'),
|
|
('biz_usage_record', '使用记录'),
|
|
('biz_return_order', '退回单'),
|
|
('biz_return_item', '退回明细'),
|
|
('biz_pallet_inventory', '托盘库存'),
|
|
('biz_pallet_event', '托盘事件'),
|
|
)
|
|
|
|
|
|
class TestBatchService:
|
|
"""测试数据批次生命周期与用户授权服务。"""
|
|
|
|
@staticmethod
|
|
def _assert_admin(current_user: CurrentUserModel) -> None:
|
|
TestBatchAccessService.assert_super_admin(current_user)
|
|
|
|
@staticmethod
|
|
def _user_identity(current_user: CurrentUserModel) -> tuple[int, str]:
|
|
user = current_user.user
|
|
if user is None or user.user_id is None or user.user_name is None:
|
|
raise PermissionException('', '当前用户信息不可用')
|
|
return user.user_id, user.user_name
|
|
|
|
@staticmethod
|
|
def _parse_purge_summary(raw_summary: Any) -> dict[str, int] | None:
|
|
if raw_summary is None:
|
|
return None
|
|
try:
|
|
parsed = json.loads(raw_summary) if isinstance(raw_summary, str) else raw_summary
|
|
except (TypeError, ValueError):
|
|
raise ServiceException(message='测试批次清理摘要格式无效') from None
|
|
if not isinstance(parsed, dict) or any(
|
|
not isinstance(key, str)
|
|
or isinstance(value, bool)
|
|
or not isinstance(value, int)
|
|
or value < 0
|
|
for key, value in parsed.items()
|
|
):
|
|
raise ServiceException(message='测试批次清理摘要格式无效')
|
|
return parsed
|
|
|
|
@classmethod
|
|
def _to_batch_model(cls, batch: BizTestBatch | Any) -> TestBatchModel:
|
|
values = {
|
|
field_name: getattr(batch, field_name, None)
|
|
for field_name in TestBatchModel.model_fields
|
|
}
|
|
values['purge_summary'] = cls._parse_purge_summary(getattr(batch, 'purge_summary', None))
|
|
return TestBatchModel.model_validate(values)
|
|
|
|
@staticmethod
|
|
def _clear_preview(batch: BizTestBatch | Any) -> None:
|
|
batch.preview_action = None
|
|
batch.preview_token_hash = None
|
|
batch.preview_scope_hash = None
|
|
batch.preview_expires_at = None
|
|
|
|
@staticmethod
|
|
def _touch(batch: BizTestBatch | Any, user_name: str, now: datetime) -> None:
|
|
batch.update_by = user_name
|
|
batch.update_time = now
|
|
|
|
@staticmethod
|
|
def _raise_for_non_active(batch: BizTestBatch | Any) -> None:
|
|
if batch.status == TestBatchStatus.CLOSED.value:
|
|
raise ServiceWarning(message='测试批次已关闭')
|
|
if batch.status == TestBatchStatus.PURGED.value:
|
|
raise ServiceWarning(message='测试批次已清理')
|
|
if batch.status != TestBatchStatus.ACTIVE.value:
|
|
raise ServiceWarning(message='测试批次状态异常')
|
|
|
|
@classmethod
|
|
async def list_available_batches(
|
|
cls,
|
|
db: AsyncSession,
|
|
current_user: CurrentUserModel,
|
|
) -> list[TestBatchOptionModel]:
|
|
user_id, _ = cls._user_identity(current_user)
|
|
is_admin = current_user.user is not None and current_user.user.admin is True
|
|
batches = await TestBatchDao.list_available_batches(db, user_id, is_admin)
|
|
return [TestBatchOptionModel.model_validate(batch) for batch in batches]
|
|
|
|
@classmethod
|
|
async def list_batches(
|
|
cls,
|
|
db: AsyncSession,
|
|
current_user: CurrentUserModel,
|
|
) -> list[TestBatchModel]:
|
|
cls._assert_admin(current_user)
|
|
batches = await TestBatchDao.list_batches(db)
|
|
return [cls._to_batch_model(batch) for batch in batches]
|
|
|
|
@classmethod
|
|
async def get_batch_detail(
|
|
cls,
|
|
db: AsyncSession,
|
|
batch_id: int,
|
|
current_user: CurrentUserModel,
|
|
) -> TestBatchDetailModel:
|
|
cls._assert_admin(current_user)
|
|
batch = await TestBatchDao.get_batch(db, batch_id)
|
|
if batch is None:
|
|
raise ServiceWarning(message='测试批次不存在')
|
|
|
|
user_ids = await TestBatchDao.list_batch_user_ids(db, batch_id)
|
|
counts = await TestBatchDao.count_owned_rows(db, batch_id)
|
|
data_summary = [
|
|
BatchImpactModel(
|
|
table_name=table_name,
|
|
label=label,
|
|
count=counts.get(table_name, 0),
|
|
keys=[],
|
|
)
|
|
for table_name, label in _OWNED_TABLE_LABELS
|
|
]
|
|
batch_model = cls._to_batch_model(batch)
|
|
return TestBatchDetailModel(
|
|
**batch_model.model_dump(),
|
|
user_ids=user_ids,
|
|
data_summary=data_summary,
|
|
)
|
|
|
|
@classmethod
|
|
async def create_batch(
|
|
cls,
|
|
db: AsyncSession,
|
|
command: TestBatchCreateModel,
|
|
current_user: CurrentUserModel,
|
|
) -> CrudResponseModel:
|
|
cls._assert_admin(current_user)
|
|
_, user_name = cls._user_identity(current_user)
|
|
batch_name = command.batch_name.strip()
|
|
if not batch_name:
|
|
raise ServiceWarning(message='测试批次名称不能为空')
|
|
|
|
now = datetime.now()
|
|
batch = BizTestBatch(
|
|
batch_code=f'TB-{now:%Y%m%d%H%M%S}-{secrets.token_hex(4).upper()}',
|
|
batch_name=batch_name,
|
|
status=TestBatchStatus.ACTIVE.value,
|
|
create_by=user_name,
|
|
create_time=now,
|
|
update_by=user_name,
|
|
update_time=now,
|
|
remark=command.remark,
|
|
)
|
|
try:
|
|
await TestBatchDao.add_batch(db, batch)
|
|
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
|
|
|
|
@classmethod
|
|
async def edit_batch(
|
|
cls,
|
|
db: AsyncSession,
|
|
batch_id: int,
|
|
command: TestBatchEditModel,
|
|
current_user: CurrentUserModel,
|
|
) -> CrudResponseModel:
|
|
cls._assert_admin(current_user)
|
|
_, user_name = cls._user_identity(current_user)
|
|
try:
|
|
batch = await TestBatchDao.get_batch_for_update(db, batch_id)
|
|
if batch is None:
|
|
raise ServiceWarning(message='测试批次不存在')
|
|
if batch.status == TestBatchStatus.PURGED.value:
|
|
raise ServiceWarning(message='测试批次已清理,不能修改')
|
|
if batch.status not in (TestBatchStatus.ACTIVE.value, TestBatchStatus.CLOSED.value):
|
|
raise ServiceWarning(message='测试批次状态异常')
|
|
|
|
now = datetime.now()
|
|
batch.remark = command.remark
|
|
cls._touch(batch, user_name, now)
|
|
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
|
|
|
|
@classmethod
|
|
async def replace_users(
|
|
cls,
|
|
db: AsyncSession,
|
|
batch_id: int,
|
|
command: TestBatchUserAssignModel,
|
|
current_user: CurrentUserModel,
|
|
) -> CrudResponseModel:
|
|
cls._assert_admin(current_user)
|
|
_, user_name = cls._user_identity(current_user)
|
|
try:
|
|
batch = await TestBatchDao.get_batch_for_update(db, batch_id)
|
|
if batch is None:
|
|
raise ServiceWarning(message='测试批次不存在')
|
|
cls._raise_for_non_active(batch)
|
|
|
|
user_ids = list(dict.fromkeys(command.user_ids))
|
|
if user_ids:
|
|
active_user_ids = await TestBatchDao.list_active_user_ids(db, user_ids)
|
|
if set(active_user_ids) != set(user_ids):
|
|
raise ServiceWarning(message='存在不存在、停用或已删除的用户')
|
|
|
|
now = datetime.now()
|
|
await TestBatchDao.replace_batch_users(db, batch_id, user_ids, user_name, now)
|
|
cls._touch(batch, user_name, now)
|
|
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
|
|
|
|
@classmethod
|
|
async def close_batch(
|
|
cls,
|
|
db: AsyncSession,
|
|
batch_id: int,
|
|
current_user: CurrentUserModel,
|
|
) -> CrudResponseModel:
|
|
cls._assert_admin(current_user)
|
|
user_id, user_name = cls._user_identity(current_user)
|
|
try:
|
|
batch = await TestBatchDao.get_batch_for_update(db, batch_id)
|
|
if batch is None:
|
|
raise ServiceWarning(message='测试批次不存在')
|
|
cls._raise_for_non_active(batch)
|
|
|
|
now = datetime.now()
|
|
batch.status = TestBatchStatus.CLOSED.value
|
|
batch.closed_by = user_id
|
|
batch.closed_at = now
|
|
cls._clear_preview(batch)
|
|
cls._touch(batch, user_name, now)
|
|
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
|
|
|
|
@classmethod
|
|
async def reopen_batch(
|
|
cls,
|
|
db: AsyncSession,
|
|
batch_id: int,
|
|
current_user: CurrentUserModel,
|
|
) -> CrudResponseModel:
|
|
cls._assert_admin(current_user)
|
|
_, user_name = cls._user_identity(current_user)
|
|
try:
|
|
batch = await TestBatchDao.get_batch_for_update(db, batch_id)
|
|
if batch is None:
|
|
raise ServiceWarning(message='测试批次不存在')
|
|
if batch.status == TestBatchStatus.PURGED.value:
|
|
raise ServiceWarning(message='测试批次已清理,不能重新开启')
|
|
if batch.status == TestBatchStatus.ACTIVE.value:
|
|
raise ServiceWarning(message='测试批次已处于启用状态')
|
|
if batch.status != TestBatchStatus.CLOSED.value:
|
|
raise ServiceWarning(message='测试批次状态异常')
|
|
|
|
now = datetime.now()
|
|
batch.status = TestBatchStatus.ACTIVE.value
|
|
batch.closed_by = None
|
|
batch.closed_at = None
|
|
cls._clear_preview(batch)
|
|
cls._touch(batch, user_name, now)
|
|
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
|