修复演示恢复默认模板覆盖逻辑
- 新增后端默认模板服务,集中维护腹腔镜胆囊切除术和头颈部CT分割的权威分类树、颜色、maskid 和层级定义。 - 演示恢复出厂设置时强制恢复系统默认模板,缺失模板会重建,已修改或删减的默认语义分类树会覆盖回默认状态。 - 清理 main.py 中重复的默认模板定义,让启动 seed 复用同一套服务逻辑,避免后续默认模板定义漂移。 - 扩展管理员恢复出厂设置测试,覆盖头颈部CT模板被改坏和腹腔镜模板缺失后的恢复结果。 - 更新 AGENTS、README 和需求/API/测试/前端审计文档,明确恢复出厂设置会权威恢复系统默认模板。
This commit is contained in:
123
backend/main.py
123
backend/main.py
@@ -24,25 +24,8 @@ logging.basicConfig(
|
||||
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
RESERVED_UNCLASSIFIED_CLASS = {
|
||||
"id": "reserved-unclassified",
|
||||
"name": "待分类",
|
||||
"color": "#000000",
|
||||
"zIndex": 0,
|
||||
"maskId": 0,
|
||||
"category": "系统保留",
|
||||
}
|
||||
|
||||
|
||||
def _with_reserved_unclassified_class(classes: list[dict]) -> list[dict]:
|
||||
filtered = [
|
||||
item for item in classes
|
||||
if item.get("id") != RESERVED_UNCLASSIFIED_CLASS["id"]
|
||||
and item.get("name") != RESERVED_UNCLASSIFIED_CLASS["name"]
|
||||
and item.get("maskId") != 0
|
||||
]
|
||||
return [*filtered, dict(RESERVED_UNCLASSIFIED_CLASS)]
|
||||
|
||||
def _ensure_runtime_schema_columns() -> None:
|
||||
"""Add nullable columns introduced after initial create_all deployments."""
|
||||
try:
|
||||
@@ -137,8 +120,6 @@ def _seed_default_project_sync() -> None:
|
||||
|
||||
def _seed_default_templates_sync() -> None:
|
||||
"""Seed default ontology templates on first startup."""
|
||||
from models import Template
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
ensure_default_templates(db)
|
||||
@@ -148,112 +129,12 @@ def _seed_default_templates_sync() -> None:
|
||||
db.close()
|
||||
|
||||
|
||||
def _template_classes(
|
||||
template_name: str,
|
||||
names: list[str],
|
||||
colors: list[tuple[int, int, int]],
|
||||
*,
|
||||
id_prefix: str,
|
||||
) -> list[dict]:
|
||||
classes = []
|
||||
for idx, (rgb, name) in enumerate(zip(colors, names)):
|
||||
color_hex = f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
|
||||
classes.append({
|
||||
"id": f"{id_prefix}-{idx}",
|
||||
"name": name,
|
||||
"color": color_hex,
|
||||
"zIndex": (len(names) - idx) * 10,
|
||||
"maskId": idx + 1,
|
||||
"category": template_name,
|
||||
})
|
||||
return classes
|
||||
|
||||
|
||||
def ensure_default_templates(db) -> None:
|
||||
"""Ensure all bundled system templates exist."""
|
||||
from models import Template
|
||||
from services.default_templates import ensure_default_templates as _ensure_default_templates
|
||||
|
||||
default_templates = [
|
||||
{
|
||||
"name": "腹腔镜胆囊切除术",
|
||||
"description": "腹腔镜胆囊切除术(LC)手术器械与解剖结构语义分割模板,共35个分类",
|
||||
"color": "#06b6d4",
|
||||
"z_index": 0,
|
||||
"classes": _with_reserved_unclassified_class(_template_classes(
|
||||
"腹腔镜胆囊切除术",
|
||||
[
|
||||
'针', '线', '肿瘤', '血管阻断夹', '棉球', '双极电凝',
|
||||
'肝脏', '胆囊', '分离钳', '脂肪', '止血海绵', '肝总管',
|
||||
'吸引器', '剪刀', '超声刀', '止血纱布', '胆总管', '生物夹',
|
||||
'无损伤钳', '钳夹', '喷洒', '胆囊管', '动脉', '电凝',
|
||||
'静脉', '标本袋', '引流管', '纱布', '金属钛夹', '韧带',
|
||||
'肝蒂', '推结器', '乳胶管-血管阻断', '吻合器', '术中超声',
|
||||
],
|
||||
[
|
||||
(134, 124, 118), (0, 157, 142), (245, 161, 0), (255, 172, 159), (146, 175, 236), (155, 62, 0),
|
||||
(255, 91, 0), (255, 234, 0), (85, 111, 181), (155, 132, 0), (181, 227, 14), (72, 0, 255),
|
||||
(255, 0, 255), (29, 32, 136), (240, 16, 116), (160, 15, 95), (0, 155, 33), (0, 160, 233),
|
||||
(52, 184, 178), (66, 115, 82), (90, 120, 41), (255, 0, 0), (117, 0, 0), (167, 24, 233),
|
||||
(42, 8, 66), (112, 113, 150), (0, 255, 0), (255, 255, 255), (0, 255, 255), (181, 85, 105),
|
||||
(113, 102, 140), (202, 202, 200), (197, 83, 181), (136, 162, 196), (138, 251, 213),
|
||||
],
|
||||
id_prefix="cls-lap",
|
||||
)),
|
||||
},
|
||||
{
|
||||
"name": "头颈部CT分割",
|
||||
"description": "头颈部CT分割",
|
||||
"color": "#ef4444",
|
||||
"z_index": 10,
|
||||
"classes": _with_reserved_unclassified_class(_template_classes(
|
||||
"头颈部CT分割",
|
||||
[
|
||||
"肿瘤/结节 (Tumor/Nodule)",
|
||||
"下颌骨 (Mandible)",
|
||||
"甲状腺 (Thyroid)",
|
||||
"气管 (Trachea)",
|
||||
"颈椎 (Cervical Spine)",
|
||||
"颈动脉 (Carotid Artery)",
|
||||
"颈静脉 (Jugular Vein)",
|
||||
"腮腺 (Parotid Gland)",
|
||||
"下颌下腺 (Submandibular Gland)",
|
||||
"舌骨 (Hyoid Bone)",
|
||||
],
|
||||
[
|
||||
(255, 0, 0),
|
||||
(0, 255, 0),
|
||||
(0, 0, 255),
|
||||
(255, 255, 0),
|
||||
(255, 0, 255),
|
||||
(0, 255, 255),
|
||||
(255, 128, 0),
|
||||
(128, 0, 128),
|
||||
(0, 128, 128),
|
||||
(128, 128, 0),
|
||||
],
|
||||
id_prefix="cls-head-neck-ct",
|
||||
)),
|
||||
},
|
||||
]
|
||||
_ensure_default_templates(db)
|
||||
|
||||
for definition in default_templates:
|
||||
existing = db.query(Template).filter(
|
||||
Template.name == definition["name"],
|
||||
Template.owner_user_id.is_(None),
|
||||
).first()
|
||||
if existing is not None:
|
||||
continue
|
||||
template = Template(
|
||||
name=definition["name"],
|
||||
description=definition["description"],
|
||||
color=definition["color"],
|
||||
z_index=definition["z_index"],
|
||||
mapping_rules={"classes": definition["classes"], "rules": []},
|
||||
owner_user_id=None,
|
||||
)
|
||||
db.add(template)
|
||||
logger.info("Seeded default template '%s' with %d classes", definition["name"], len(definition["classes"]))
|
||||
db.commit()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
|
||||
Reference in New Issue
Block a user