安全:增加测试批次原子清理

This commit is contained in:
焦龙言
2026-09-23 14:52:07 +08:00
parent b11c457e61
commit 1aaed57825
6 changed files with 966 additions and 1 deletions
@@ -1,10 +1,11 @@
import hashlib
import heapq
import json
import secrets
from collections import defaultdict, deque
from dataclasses import dataclass
from sqlalchemy import Column, select
from sqlalchemy import Column, MetaData, select
from sqlalchemy.ext.asyncio import AsyncSession
from config.database import Base
@@ -98,6 +99,25 @@ BATCH_LABELS = {
'biz_pallet_event': '托盘事件',
}
_DELETE_TIE_BREAK_ORDER = (
'biz_pallet_inventory',
'biz_usage_record',
'biz_return_item',
'biz_shipment_item',
'biz_inbound_item',
'biz_return_order',
'biz_shipment_order',
'biz_inbound_order',
'biz_pallet_event',
'biz_filling_record',
'biz_production_line',
'biz_customer_site',
'biz_customer',
'biz_warehouse',
'biz_strain',
'biz_pallet',
)
ROOT_TABLES = {
'strain': 'biz_strain',
'customer': 'biz_customer',
@@ -135,6 +155,42 @@ def issue_preview_token() -> tuple[str, str]:
return raw, hashlib.sha256(raw.encode()).hexdigest()
def derive_batch_delete_order(metadata: MetaData) -> list[str]:
"""从实际外键图生成子表优先的确定性删除顺序。"""
safe_tables = set(BATCH_MODELS)
metadata_batch_tables = {table.name for table in metadata.tables.values() if 'test_batch_id' in table.columns}
if metadata_batch_tables != safe_tables or set(_DELETE_TIE_BREAK_ORDER) != safe_tables:
raise RuntimeError('测试批次清理安全表集合不一致')
children_by_parent: dict[str, set[str]] = {table_name: set() for table_name in safe_tables}
parents_by_child: dict[str, set[str]] = {table_name: set() for table_name in safe_tables}
for child_name in sorted(safe_tables):
for foreign_key in metadata.tables[child_name].foreign_keys:
parent_name = foreign_key.target_fullname.rsplit('.', maxsplit=1)[0]
if parent_name not in safe_tables:
continue
children_by_parent[parent_name].add(child_name)
parents_by_child[child_name].add(parent_name)
priority = {table_name: index for index, table_name in enumerate(_DELETE_TIE_BREAK_ORDER)}
ready = [(priority[table_name], table_name) for table_name, children in children_by_parent.items() if not children]
heapq.heapify(ready)
order: list[str] = []
while ready:
_, child_name = heapq.heappop(ready)
order.append(child_name)
for parent_name in sorted(parents_by_child[child_name]):
children_by_parent[parent_name].remove(child_name)
if not children_by_parent[parent_name]:
heapq.heappush(ready, (priority[parent_name], parent_name))
if len(order) != len(safe_tables):
raise RuntimeError('测试批次清理外键图存在环')
if set(order) != safe_tables:
raise RuntimeError('测试批次清理安全表集合不一致')
return order
@dataclass(frozen=True)
class _GraphEdge:
source_table: str