eric.w 2 hete
szülő
commit
6ed3036190

+ 172 - 0
BridgeDiseaseBackend-main/scripts/export_db_snapshot.py

@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+"""
+将当前 MySQL 演示数据与静态资源导出到仓库,便于他人 clone 后复现相同界面效果。
+
+生成物:
+  sql/seed_snapshot.sql          — user / model / media / detection / operation 全量数据
+  seed_assets/snapshot/static/   — medias、models、results、avatars
+  seed_assets/snapshot/training_meta/ — 训练任务与数据集元数据 JSON
+  seed_assets/snapshot/manifest.json   — 导出摘要
+
+用法(项目根或 BridgeDiseaseBackend-main 下,需 MySQL 3307 可连):
+  python scripts/export_db_snapshot.py
+"""
+from __future__ import annotations
+
+import json
+import os
+import shutil
+import subprocess
+import sys
+from datetime import datetime, timezone
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+SQL_OUT = ROOT / 'sql' / 'seed_snapshot.sql'
+SNAPSHOT_ROOT = ROOT / 'seed_assets' / 'snapshot'
+STATIC_SRC = ROOT / 'app' / 'static'
+STATIC_DST = SNAPSHOT_ROOT / 'static'
+TRAINING_SRC = ROOT / 'data' / 'training_meta'
+TRAINING_DST = SNAPSHOT_ROOT / 'training_meta'
+
+TABLES = ('user', 'model', 'media', 'detection', 'operation')
+STATIC_DIRS = ('medias', 'models', 'results', 'avatars')
+
+
+def _db_env():
+    uri = os.environ.get(
+        'SQLALCHEMY_DATABASE_URI',
+        'mysql+pymysql://root:bridgedisease_root@127.0.0.1:3307/bridge_disease?charset=utf8mb4',
+    )
+    # mysql+pymysql://user:pass@host:port/db?...
+    body = uri.split('://', 1)[-1]
+    auth, rest = body.split('@', 1)
+    user, password = auth.split(':', 1)
+    host_port, db_part = rest.split('/', 1)
+    database = db_part.split('?', 1)[0]
+    host, port = (host_port.split(':', 1) + ['3306'])[:2]
+    return {
+        'host': host,
+        'port': port,
+        'user': user,
+        'password': password,
+        'database': database,
+    }
+
+
+def dump_mysql(cfg: dict) -> None:
+    SQL_OUT.parent.mkdir(parents=True, exist_ok=True)
+    header = (
+        '-- 检澜 DockScope 演示数据快照(由 scripts/export_db_snapshot.py 生成)\n'
+        f"-- 导出时间 UTC: {datetime.now(timezone.utc).isoformat()}\n"
+        'SET NAMES utf8mb4;\n'
+        'SET FOREIGN_KEY_CHECKS=0;\n'
+        'SET sql_mode = \'NO_AUTO_VALUE_ON_ZERO\';\n\n'
+    )
+    cmd = [
+        'mysqldump',
+        f"--host={cfg['host']}",
+        f"--port={cfg['port']}",
+        f"-u{cfg['user']}",
+        f"-p{cfg['password']}",
+        '--default-character-set=utf8mb4',
+        '--no-create-info',
+        '--skip-triggers',
+        '--complete-insert',
+        '--hex-blob',
+        cfg['database'],
+        *TABLES,
+    ]
+    try:
+        proc = subprocess.run(cmd, capture_output=True, check=True)
+    except FileNotFoundError:
+        # 尝试 Docker 中的 mysql 客户端
+        docker_cmd = [
+            'docker',
+            'exec',
+            'bridge-disease-mysql',
+            'mysqldump',
+            f"-u{cfg['user']}",
+            f"-p{cfg['password']}",
+            '--default-character-set=utf8mb4',
+            '--no-create-info',
+            '--skip-triggers',
+            '--complete-insert',
+            '--hex-blob',
+            cfg['database'],
+            *TABLES,
+        ]
+        proc = subprocess.run(docker_cmd, capture_output=True, check=True)
+    body = proc.stdout.decode('utf-8', errors='replace')
+    footer = '\nSET FOREIGN_KEY_CHECKS=1;\n'
+    SQL_OUT.write_text(header + body + footer, encoding='utf-8')
+    print(f'[ok] SQL -> {SQL_OUT} ({SQL_OUT.stat().st_size // 1024} KB)')
+
+
+def copy_tree(src: Path, dst: Path) -> int:
+    if not src.is_dir():
+        return 0
+    if dst.exists():
+        shutil.rmtree(dst)
+    shutil.copytree(src, dst)
+    count = sum(1 for _ in dst.rglob('*') if _.is_file())
+    print(f'[ok] {src.name} -> {dst} ({count} files)')
+    return count
+
+
+def main() -> int:
+    cfg = _db_env()
+    print(f"Export from {cfg['user']}@{cfg['host']}:{cfg['port']}/{cfg['database']}")
+
+    dump_mysql(cfg)
+
+    STATIC_DST.parent.mkdir(parents=True, exist_ok=True)
+    file_counts = {}
+    for name in STATIC_DIRS:
+        src = STATIC_SRC / name
+        if src.is_dir():
+            file_counts[name] = copy_tree(src, STATIC_DST / name)
+
+    # 用户头像(SQL 引用 static/avatars/1~3.jpg)
+    avatars_dst = STATIC_DST / 'avatars'
+    avatars_dst.mkdir(parents=True, exist_ok=True)
+    media_pool = list((STATIC_SRC / 'medias').glob('*.jpg')) or list(
+        (ROOT / 'seed_assets' / 'medias').glob('*.jpg')
+    )
+    for i in range(1, 4):
+        dst = avatars_dst / f'{i}.jpg'
+        if not dst.is_file() and media_pool:
+            shutil.copy2(media_pool[(i - 1) % len(media_pool)], dst)
+    if avatars_dst.is_dir():
+        file_counts['avatars'] = sum(1 for _ in avatars_dst.glob('*.jpg'))
+        print(f'[ok] avatars -> {avatars_dst} ({file_counts["avatars"]} files)')
+
+    training_files = 0
+    if TRAINING_SRC.is_dir():
+        TRAINING_DST.parent.mkdir(parents=True, exist_ok=True)
+        if TRAINING_DST.exists():
+            shutil.rmtree(TRAINING_DST)
+        shutil.copytree(TRAINING_SRC, TRAINING_DST)
+        training_files = sum(1 for _ in TRAINING_DST.rglob('*') if _.is_file())
+        print(f'[ok] training_meta -> {TRAINING_DST} ({training_files} files)')
+
+    manifest = {
+        'exported_at': datetime.now(timezone.utc).isoformat(),
+        'database': cfg['database'],
+        'tables': list(TABLES),
+        'static_files': file_counts,
+        'training_meta_files': training_files,
+        'sql_file': 'sql/seed_snapshot.sql',
+    }
+    SNAPSHOT_ROOT.mkdir(parents=True, exist_ok=True)
+    manifest_path = SNAPSHOT_ROOT / 'manifest.json'
+    manifest_path.write_text(
+        json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8'
+    )
+    print(f'[ok] manifest -> {manifest_path}')
+    print('\n请提交 sql/seed_snapshot.sql 与 seed_assets/snapshot/ 到 Git,供他人运行 import_db_snapshot.py')
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 134 - 0
BridgeDiseaseBackend-main/scripts/import_db_snapshot.py

@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+"""
+导入 export_db_snapshot.py 生成的演示快照,并还原静态文件与训练元数据。
+
+用法(空库或需覆盖演示数据时):
+  cd BridgeDiseaseBackend-main
+  $env:SQLALCHEMY_DATABASE_URI="mysql+pymysql://root:bridgedisease_root@127.0.0.1:3307/bridge_disease?charset=utf8mb4"
+  python scripts/import_db_snapshot.py
+
+可选:仅还原文件不导入 SQL
+  python scripts/import_db_snapshot.py --files-only
+"""
+from __future__ import annotations
+
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+SQL_FILE = ROOT / 'sql' / 'seed_snapshot.sql'
+SNAPSHOT_STATIC = ROOT / 'seed_assets' / 'snapshot' / 'static'
+SNAPSHOT_TRAINING = ROOT / 'seed_assets' / 'snapshot' / 'training_meta'
+STATIC_DST = ROOT / 'app' / 'static'
+TRAINING_DST = ROOT / 'data' / 'training_meta'
+
+sys.path.insert(0, str(ROOT))
+os.environ.setdefault(
+    'SQLALCHEMY_DATABASE_URI',
+    'mysql+pymysql://root:bridgedisease_root@127.0.0.1:3307/bridge_disease?charset=utf8mb4',
+)
+
+
+def _db_env():
+    uri = os.environ['SQLALCHEMY_DATABASE_URI']
+    body = uri.split('://', 1)[-1]
+    auth, rest = body.split('@', 1)
+    user, password = auth.split(':', 1)
+    host_port, db_part = rest.split('/', 1)
+    database = db_part.split('?', 1)[0]
+    host, port = (host_port.split(':', 1) + ['3306'])[:2]
+    return host, port, user, password, database
+
+
+def import_sql() -> None:
+    if not SQL_FILE.is_file():
+        print(f'缺少 {SQL_FILE},请先运行 export_db_snapshot.py', file=sys.stderr)
+        sys.exit(1)
+    host, port, user, password, database = _db_env()
+    from app import create_app
+    from app.models import db
+
+    app = create_app()
+    from sqlalchemy import text
+
+    with app.app_context():
+        db.create_all()
+        # 清空业务表(保留表结构)
+        db.session.execute(text('SET FOREIGN_KEY_CHECKS=0'))
+        for table in ('detection', 'operation', 'media', 'model', 'user'):
+            db.session.execute(text(f'TRUNCATE TABLE `{table}`'))
+        db.session.commit()
+        db.session.execute(text('SET FOREIGN_KEY_CHECKS=1'))
+        db.session.commit()
+        print('[ok] 已清空 user/model/media/detection/operation')
+
+    cmd = [
+        'mysql',
+        f'--host={host}',
+        f'--port={port}',
+        f'-u{user}',
+        f'-p{password}',
+        '--default-character-set=utf8mb4',
+        database,
+    ]
+    try:
+        with open(SQL_FILE, 'rb') as f:
+            subprocess.run(cmd, stdin=f, check=True)
+    except FileNotFoundError:
+        docker_cmd = [
+            'docker',
+            'exec',
+            '-i',
+            'bridge-disease-mysql',
+            'mysql',
+            f'-u{user}',
+            f'-p{password}',
+            '--default-character-set=utf8mb4',
+            database,
+        ]
+        with open(SQL_FILE, 'rb') as f:
+            subprocess.run(docker_cmd, stdin=f, check=True)
+    print(f'[ok] 已导入 {SQL_FILE}')
+
+
+def restore_files() -> None:
+    if not SNAPSHOT_STATIC.is_dir():
+        print(f'缺少 {SNAPSHOT_STATIC},跳过静态文件还原')
+        return
+    STATIC_DST.mkdir(parents=True, exist_ok=True)
+    for child in SNAPSHOT_STATIC.iterdir():
+        dst = STATIC_DST / child.name
+        if dst.exists():
+            shutil.rmtree(dst)
+        shutil.copytree(child, dst)
+        n = sum(1 for _ in dst.rglob('*') if _.is_file())
+        print(f'[ok] 静态 {child.name} -> {dst} ({n} files)')
+
+    if SNAPSHOT_TRAINING.is_dir():
+        TRAINING_DST.mkdir(parents=True, exist_ok=True)
+        for f in SNAPSHOT_TRAINING.glob('*.json'):
+            shutil.copy2(f, TRAINING_DST / f.name)
+        print(f'[ok] training_meta -> {TRAINING_DST}')
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--files-only', action='store_true', help='仅还原静态与 training_meta')
+    parser.add_argument('--sql-only', action='store_true', help='仅导入 SQL')
+    args = parser.parse_args()
+
+    if not args.files_only:
+        import_sql()
+    if not args.sql_only:
+        restore_files()
+
+    print('\n完成。请启动后端并刷新前端(admin / Admin123456)。')
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 44 - 0
BridgeDiseaseBackend-main/seed_assets/snapshot/README.md

@@ -0,0 +1,44 @@
+# 检澜演示环境快照
+
+由 `scripts/export_db_snapshot.py` 生成,供新同事 **clone 后一键还原** 与当前演示一致的数据。
+
+## 包含内容
+
+| 路径 | 说明 |
+|------|------|
+| `sql/seed_snapshot.sql` | MySQL 数据:`user`、`model`、`media`、`detection`、`operation` |
+| `static/medias/` | 媒体库图片 |
+| `static/models/` | YOLO 权重(含种子 7 类 + 训练产出,体积较大) |
+| `static/results/` | 检测标注预览图 |
+| `static/avatars/` | 用户头像占位图 |
+| `training_meta/` | 模型训练任务与数据集 JSON |
+| `manifest.json` | 导出时间与文件统计 |
+
+## 导入(推荐)
+
+```powershell
+cd BridgeDiseaseBackend-main
+docker compose -p bridge-disease up -d db   # 或已有 MySQL 3307
+
+$env:SQLALCHEMY_DATABASE_URI="mysql+pymysql://root:bridgedisease_root@127.0.0.1:3307/bridge_disease?charset=utf8mb4"
+python scripts/import_db_snapshot.py
+python run.py
+```
+
+仅还原文件、不改数据库:`python scripts/import_db_snapshot.py --files-only`
+
+## 重新导出(维护者)
+
+本地环境调试满意后:
+
+```powershell
+python scripts/export_db_snapshot.py
+git add sql/seed_snapshot.sql seed_assets/snapshot/
+```
+
+> `static/models/` 约 80MB+,推送前确认团队可接受仓库体积,或改用 Git LFS。
+
+## 与 `init_db.sql` 的关系
+
+- **`init_db.sql`**:最小种子(用户 + 7 个演示模型元数据),需再跑 `seed_medias.py` 等脚本。
+- **`seed_snapshot.sql`**:当前完整演示库 + 快照静态文件,**效果与维护者本机一致**。

+ 19 - 0
BridgeDiseaseBackend-main/seed_assets/snapshot/manifest.json

@@ -0,0 +1,19 @@
+{
+  "exported_at": "2026-06-03T07:57:03.244666+00:00",
+  "database": "bridge_disease",
+  "tables": [
+    "user",
+    "model",
+    "media",
+    "detection",
+    "operation"
+  ],
+  "static_files": {
+    "medias": 8,
+    "models": 12,
+    "results": 19,
+    "avatars": 3
+  },
+  "training_meta_files": 2,
+  "sql_file": "sql/seed_snapshot.sql"
+}

+ 20 - 0
BridgeDiseaseBackend-main/seed_assets/snapshot/training_meta/datasets.json

@@ -0,0 +1,20 @@
+[
+  {
+    "dataset_id": "76e50e005494",
+    "name": "bridge_hazard_demo",
+    "data_yaml": "C:\\pro\\kadupul\\BridgeDiseaseBackend-main\\data\\datasets\\76e50e005494\\extracted\\bridge_hazard_demo\\data.yaml",
+    "root_path": "C:\\pro\\kadupul\\BridgeDiseaseBackend-main\\data\\datasets\\76e50e005494\\extracted",
+    "owner_id": 1,
+    "owner_username": "admin",
+    "created_at": "2026-06-03T15:29:31.176256+08:00"
+  },
+  {
+    "dataset_id": "9c0ff1d8e3ba",
+    "name": "bridge_hazard_demo",
+    "data_yaml": "C:\\pro\\kadupul\\BridgeDiseaseBackend-main\\data\\datasets\\9c0ff1d8e3ba\\extracted\\bridge_hazard_demo\\data.yaml",
+    "root_path": "C:\\pro\\kadupul\\BridgeDiseaseBackend-main\\data\\datasets\\9c0ff1d8e3ba\\extracted",
+    "owner_id": 1,
+    "owner_username": "admin",
+    "created_at": "2026-06-03T15:20:45.389999+08:00"
+  }
+]

+ 68 - 0
BridgeDiseaseBackend-main/seed_assets/snapshot/training_meta/jobs.json

@@ -0,0 +1,68 @@
+[
+  {
+    "job_id": "cae074c0fbce",
+    "run_name": "train_cae074c0fbce",
+    "status": "completed",
+    "progress": 100,
+    "owner_id": 1,
+    "owner_username": "admin",
+    "dataset_id": "76e50e005494",
+    "dataset_name": "bridge_hazard_demo",
+    "data_yaml": "C:\\pro\\kadupul\\BridgeDiseaseBackend-main\\data\\datasets\\76e50e005494\\extracted\\bridge_hazard_demo\\data.yaml",
+    "base_model": "yolov8n-seg.pt",
+    "disease_category": "钢构腐蚀",
+    "output_model_name": "re.pt",
+    "epochs": 1,
+    "imgsz": 640,
+    "batch": 1,
+    "augmentation": "随机点+颜色扭曲+高斯模糊",
+    "register_to_library": true,
+    "logs": [
+      "[15:36:27] 开始加载基线权重…",
+      "[15:36:27] 数据集配置:C:\\pro\\kadupul\\BridgeDiseaseBackend-main\\data\\datasets\\76e50e005494\\extracted\\bridge_hazard_demo\\_dockscope_data.yaml",
+      "[15:36:27] 训练设备:cpu",
+      "[15:36:27] 启动 YOLOv8 分割训练:epochs=1, imgsz=640",
+      "[15:36:32] Epoch 1/1 完成",
+      "[15:36:41] 训练完成,best 权重:best.pt",
+      "[15:36:42] 已写入模型库:re.pt"
+    ],
+    "created_at": "2026-06-03T15:36:27.633791+08:00",
+    "updated_at": "2026-06-03T15:36:42.155850+08:00",
+    "started_at": "2026-06-03T15:36:27.636466+08:00",
+    "current_epoch": 1,
+    "total_epochs": 1,
+    "finished_at": "2026-06-03T15:36:42.154661+08:00",
+    "best_weights": "C:\\pro\\kadupul\\BridgeDiseaseBackend-main\\data\\training_runs\\train_cae074c0fbce\\weights\\best.pt",
+    "metrics": {
+      "box_p": 0.0,
+      "box_r": 0.0,
+      "box_mAP50": 0.0,
+      "box_mAP50_95": 0.0,
+      "mask_p": 0.0,
+      "mask_r": 0.0,
+      "mask_mAP50": 0.0,
+      "mask_mAP50_95": 0.0
+    },
+    "registered_model": {
+      "model_id": 11,
+      "model_name": "re.pt",
+      "model_path": "static/models/re.pt",
+      "disease_category": "钢构腐蚀",
+      "augmentation": "随机点+颜色扭曲+高斯模糊",
+      "layers": 262,
+      "parameters": 3264006,
+      "GFLOPs": 35.3,
+      "box_p": 0.0,
+      "box_r": 0.0,
+      "box_mAP50": 0.0,
+      "box_mAP50_95": 0.0,
+      "mask_p": 0.0,
+      "mask_r": 0.0,
+      "mask_mAP50": 0.0,
+      "mask_mAP50_95": 0.0,
+      "f1_score": 0.0,
+      "fitness_score": 0.0,
+      "created_at": "2026-06-03T15:36:42+08:00"
+    }
+  }
+]

+ 18 - 0
BridgeDiseaseBackend-main/sql/README.md

@@ -0,0 +1,18 @@
+# SQL 种子文件
+
+| 文件 | 用途 |
+|------|------|
+| `init_db.sql` | 最小初始化:3 个账号 + 7 个演示模型元数据(不含媒体/检测行) |
+| `seed_snapshot.sql` | **完整演示快照**(用户、11 个模型、8 个媒体、20 条检测、操作日志) |
+
+新环境推荐:
+
+```powershell
+python scripts/import_db_snapshot.py
+```
+
+生成/更新快照:
+
+```powershell
+python scripts/export_db_snapshot.py
+```

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 39 - 0
BridgeDiseaseBackend-main/sql/seed_snapshot.sql


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott