197 lines
8.0 KiB
Python
197 lines
8.0 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from common.vo import CrudResponseModel
|
|
from config.database import Base
|
|
from config.env import TestBatchConfig
|
|
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.dao.test_batch_purge_dao import TestBatchPurgeDao
|
|
from module_business.domain.enums import TestBatchStatus
|
|
from module_business.domain.test_batch_graph import (
|
|
BATCH_LABELS,
|
|
BATCH_MODELS,
|
|
BatchSelection,
|
|
derive_batch_delete_order,
|
|
issue_preview_token,
|
|
selection_hash,
|
|
)
|
|
from module_business.entity.vo.test_batch_vo import (
|
|
BatchImpactModel,
|
|
PurgeExecuteRequestModel,
|
|
PurgePreviewModel,
|
|
)
|
|
from module_business.service.test_batch_access_service import TestBatchAccessService
|
|
|
|
|
|
class TestBatchPurgeService:
|
|
"""预览并原子清理已关闭的测试批次。"""
|
|
|
|
@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 _assert_closed(batch: Any | None) -> None:
|
|
if batch is None:
|
|
raise ServiceWarning('', '测试批次不存在')
|
|
if batch.status == TestBatchStatus.PURGED.value:
|
|
raise ServiceWarning('', '测试批次已清理')
|
|
if batch.status == TestBatchStatus.ACTIVE.value:
|
|
raise ServiceWarning('', '测试批次必须先关闭')
|
|
if batch.status != TestBatchStatus.CLOSED.value:
|
|
raise ServiceWarning('', '测试批次状态异常')
|
|
|
|
@staticmethod
|
|
def _assert_token(batch: object, raw_token: str, now: datetime) -> None:
|
|
supplied_hash = hashlib.sha256(raw_token.encode()).hexdigest()
|
|
stored_hash = getattr(batch, 'preview_token_hash', None)
|
|
comparable_hash = stored_hash if isinstance(stored_hash, str) else '0' * 64
|
|
if not hmac.compare_digest(comparable_hash, supplied_hash):
|
|
raise ServiceWarning('', '预览令牌无效')
|
|
if getattr(batch, 'preview_action', None) != 'PURGE':
|
|
raise ServiceWarning('', '预览令牌无效')
|
|
expires_at = getattr(batch, 'preview_expires_at', None)
|
|
if expires_at is None or expires_at <= now:
|
|
raise ServiceWarning('', '预览令牌已过期')
|
|
|
|
@staticmethod
|
|
def _counts(selection: BatchSelection) -> dict[str, int]:
|
|
return {table_name: len(selection[table_name]) for table_name in BATCH_MODELS}
|
|
|
|
@staticmethod
|
|
def _blocking_reason(total_count: int, limit: int) -> str | None:
|
|
if limit <= 0:
|
|
return '尚未配置在线测试批次清理上限'
|
|
if total_count > limit:
|
|
return '批次数据量超过在线清理上限,请按运维流程处理'
|
|
return None
|
|
|
|
@classmethod
|
|
async def preview(
|
|
cls,
|
|
db: AsyncSession,
|
|
batch_id: int,
|
|
current_user: CurrentUserModel,
|
|
) -> PurgePreviewModel:
|
|
cls._assert_admin(current_user)
|
|
_, user_name = cls._user_identity(current_user)
|
|
try:
|
|
batch = await TestBatchDao.get_batch_for_update(db, batch_id)
|
|
cls._assert_closed(batch)
|
|
selection = await TestBatchPurgeDao.load_selection(db, batch_id, for_update=False)
|
|
display_keys = await TestBatchPurgeDao.list_display_keys(db, selection)
|
|
counts = cls._counts(selection)
|
|
total_count = sum(counts.values())
|
|
limit = TestBatchConfig.test_batch_online_purge_max_rows
|
|
blocking_reason = cls._blocking_reason(total_count, limit)
|
|
|
|
now = datetime.now()
|
|
expires_at = now + timedelta(seconds=TestBatchConfig.test_batch_preview_ttl_seconds)
|
|
raw_token, token_hash = issue_preview_token()
|
|
await TestBatchDao.store_preview(
|
|
db,
|
|
batch,
|
|
action='PURGE',
|
|
token_hash=token_hash,
|
|
scope_hash=selection_hash(selection),
|
|
expires_at=expires_at,
|
|
update_by=user_name,
|
|
update_time=now,
|
|
)
|
|
impacts = [
|
|
BatchImpactModel(
|
|
table_name=table_name,
|
|
label=BATCH_LABELS[table_name],
|
|
count=counts[table_name],
|
|
keys=display_keys[table_name],
|
|
)
|
|
for table_name in BATCH_MODELS
|
|
]
|
|
response = PurgePreviewModel(
|
|
batch_id=batch_id,
|
|
batch_name=batch.batch_name,
|
|
preview_token=raw_token,
|
|
expires_at=expires_at,
|
|
total_count=total_count,
|
|
impacts=impacts,
|
|
online_purge_allowed=blocking_reason is None,
|
|
online_purge_limit=limit,
|
|
blocking_reason=blocking_reason,
|
|
)
|
|
await db.commit()
|
|
return response
|
|
except Exception:
|
|
await db.rollback()
|
|
raise
|
|
|
|
@classmethod
|
|
async def purge(
|
|
cls,
|
|
db: AsyncSession,
|
|
batch_id: int,
|
|
command: PurgeExecuteRequestModel,
|
|
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 not None and batch.status == TestBatchStatus.PURGED.value:
|
|
response = CrudResponseModel(is_success=True, message='该测试批次已清理')
|
|
await db.commit()
|
|
return response
|
|
|
|
cls._assert_closed(batch)
|
|
if command.batch_name != batch.batch_name:
|
|
raise ServiceWarning('', '测试批次名称不匹配')
|
|
cls._assert_token(batch, command.preview_token, datetime.now())
|
|
|
|
selection = await TestBatchPurgeDao.load_selection(db, batch_id, for_update=True)
|
|
if selection_hash(selection) != batch.preview_scope_hash:
|
|
raise ServiceWarning('', '预览范围已变化')
|
|
counts = cls._counts(selection)
|
|
total_count = sum(counts.values())
|
|
blocking_reason = cls._blocking_reason(
|
|
total_count,
|
|
TestBatchConfig.test_batch_online_purge_max_rows,
|
|
)
|
|
if blocking_reason is not None:
|
|
raise ServiceWarning('', blocking_reason)
|
|
|
|
await TestBatchPurgeDao.delete_user_warehouse_links(db, selection['biz_warehouse'])
|
|
await TestBatchPurgeDao.delete_user_site_links(db, selection['biz_customer_site'])
|
|
for table_name in derive_batch_delete_order(Base.metadata):
|
|
deleted = await TestBatchPurgeDao.delete_table_rows(db, table_name, batch_id)
|
|
if deleted != counts[table_name]:
|
|
raise ServiceException(message=f'{table_name} 清理数量发生变化')
|
|
await TestBatchDao.delete_batch_users(db, batch_id)
|
|
|
|
now = datetime.now()
|
|
batch.status = TestBatchStatus.PURGED.value
|
|
batch.purged_by = user_id
|
|
batch.purged_at = now
|
|
batch.purge_summary = json.dumps(counts, ensure_ascii=False, sort_keys=True)
|
|
batch.update_by = user_name
|
|
batch.update_time = now
|
|
await TestBatchDao.clear_preview(db, batch)
|
|
response = CrudResponseModel(is_success=True, message='测试批次清理成功')
|
|
await db.commit()
|
|
return response
|
|
except Exception:
|
|
await db.rollback()
|
|
raise
|