<mxfile host="app.diagrams.net" agent="Cursor" version="24.0.0" type="device">
  <diagram id="overall" name="1-系统总体架构">
    <mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1200" math="0" shadow="0">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>

        <!-- Title -->
        <mxCell id="title1" value="banksys_szai4 系统总体架构&#xa;（银行营销数据分析与认购预测 · 单体 Streamlit 应用）" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="400" y="20" width="800" height="50" as="geometry"/>
        </mxCell>

        <!-- User Layer -->
        <mxCell id="user-layer" value="用户访问层" style="swimlane;horizontal=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=1;startSize=30;" vertex="1" parent="1">
          <mxGeometry x="40" y="90" width="1520" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="browser" value="浏览器&#xa;HTTP / WebSocket" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="user-layer">
          <mxGeometry x="60" y="20" width="200" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="endpoints" value="Streamlit 端点&#xa;/  (首页 main.py)&#xa;/01_data_analysis&#xa;/02_prediction&#xa;/_stcore/health (健康检查)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=8;" vertex="1" parent="user-layer">
          <mxGeometry x="320" y="10" width="280" height="80" as="geometry"/>
        </mxCell>

        <!-- Deploy Layer -->
        <mxCell id="deploy-layer" value="部署运行层" style="swimlane;horizontal=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontStyle=1;startSize=30;" vertex="1" parent="1">
          <mxGeometry x="40" y="220" width="1520" height="200" as="geometry"/>
        </mxCell>
        <mxCell id="host" value="生产服务器&#xa;/opt/banksys&#xa;主机端口 8004" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="deploy-layer">
          <mxGeometry x="60" y="30" width="180" height="70" as="geometry"/>
        </mxCell>
        <mxCell id="docker" value="Docker 容器 banksys&#xa;python:3.11-slim&#xa;容器端口 8501&#xa;restart: unless-stopped" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="deploy-layer">
          <mxGeometry x="300" y="20" width="220" height="90" as="geometry"/>
        </mxCell>
        <mxCell id="streamlit-srv" value="Streamlit Server&#xa;streamlit run app/main.py&#xa;--server.port=8501&#xa;--server.address=0.0.0.0" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" vertex="1" parent="deploy-layer">
          <mxGeometry x="580" y="20" width="260" height="90" as="geometry"/>
        </mxCell>
        <mxCell id="healthcheck" value="HEALTHCHECK&#xa;GET /_stcore/health&#xa;interval=30s" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="deploy-layer">
          <mxGeometry x="900" y="30" width="180" height="70" as="geometry"/>
        </mxCell>
        <mxCell id="build-train" value="镜像构建期训练&#xa;RUN python -m app.ml.train --overwrite&#xa;model.pkl 烤入镜像" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=8;" vertex="1" parent="deploy-layer">
          <mxGeometry x="60" y="120" width="340" height="60" as="geometry"/>
        </mxCell>

        <!-- App Core -->
        <mxCell id="app-layer" value="应用核心（单进程 Python 单体）" style="swimlane;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;startSize=30;" vertex="1" parent="1">
          <mxGeometry x="40" y="450" width="1520" height="280" as="geometry"/>
        </mxCell>
        <mxCell id="ui-box" value="UI 层 (Streamlit SSR)&#xa;app/main.py — 首页&#xa;app/pages/01_data_analysis.py — 数据分析&#xa;app/pages/02_prediction.py — 在线预测" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=8;" vertex="1" parent="app-layer">
          <mxGeometry x="30" y="40" width="340" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="bl-box" value="业务逻辑层 app/models/&#xa;data_loader.py — CSV 加载/校验/Schema&#xa;visualizer.py — 统计计算 + Plotly 图表&#xa;predictor.py — 在线预测服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=8;" vertex="1" parent="app-layer">
          <mxGeometry x="420" y="40" width="360" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="ml-box" value="机器学习层 app/ml/&#xa;train.py — 离线训练 CLI&#xa;RandomForest + OneHotEncoder Pipeline&#xa;app/ml/model/model.pkl — 模型产物" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=8;" vertex="1" parent="app-layer">
          <mxGeometry x="830" y="40" width="340" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="cache-box" value="进程内缓存&#xa;@st.cache_data — 数据/选项缓存&#xa;@lru_cache — 模型懒加载缓存" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;align=left;spacingLeft=8;" vertex="1" parent="app-layer">
          <mxGeometry x="1220" y="40" width="260" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="tech-stack" value="技术栈：Python 3.11 | Streamlit ≥1.28 | pandas ≥2.0 | scikit-learn ≥1.3 | Plotly ≥5.18 | joblib&#xa;无 REST API · 无数据库 · 无微服务 · 无消息队列 · 无 Redis" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;align=left;spacingLeft=8;fontStyle=2" vertex="1" parent="app-layer">
          <mxGeometry x="30" y="170" width="1450" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="tests-box" value="测试 tests/ — pytest + ruff (CI)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="app-layer">
          <mxGeometry x="30" y="230" width="300" height="40" as="geometry"/>
        </mxCell>

        <!-- Data Layer -->
        <mxCell id="data-layer" value="数据层（CSV 文件存储，无数据库）" style="swimlane;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1;startSize=30;" vertex="1" parent="1">
          <mxGeometry x="40" y="760" width="1520" height="160" as="geometry"/>
        </mxCell>
        <mxCell id="train-csv" value="data/train.csv&#xa;22 列（含 subscribe 目标列）&#xa;~22500 行" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="data-layer">
          <mxGeometry x="200" y="40" width="200" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="test-csv" value="data/test.csv&#xa;21 列（无 subscribe）&#xa;~7500 行" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="data-layer">
          <mxGeometry x="500" y="40" width="200" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="model-pkl" value="app/ml/model/model.pkl&#xa;Pipeline + 特征 Schema&#xa;(.gitignore, 运行时/构建时生成)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="data-layer">
          <mxGeometry x="800" y="40" width="260" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="schema-info" value="特征 Schema (data_loader.py)&#xa;分类 10: job, marital, education, default, housing, loan, contact, month, day_of_week, poutcome&#xa;数值 10: age, duration, campaign, pdays, previous, emp_var_rate, cons_price_index, cons_conf_index, lending_rate3m, nr_employed&#xa;目标: subscribe (yes/no)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;align=left;spacingLeft=8;fontSize=11;" vertex="1" parent="data-layer">
          <mxGeometry x="1100" y="30" width="380" height="110" as="geometry"/>
        </mxCell>

        <!-- CI/CD -->
        <mxCell id="cicd-layer" value="CI/CD (GitHub Actions)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontStyle=1;startSize=30;" vertex="1" parent="1">
          <mxGeometry x="40" y="950" width="1520" height="130" as="geometry"/>
        </mxCell>
        <mxCell id="ci" value="CI (.github/workflows/ci.yml)&#xa;push main/feature/** | PR → main&#xa;ruff format → ruff check → pytest --cov → docker build" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;spacingLeft=8;" vertex="1" parent="cicd-layer">
          <mxGeometry x="60" y="40" width="420" height="70" as="geometry"/>
        </mxCell>
        <mxCell id="cd" value="CD (.github/workflows/cd.yml)&#xa;push main → SSH 部署&#xa;git pull → docker build → docker run -p 8004:8501 → curl health" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;spacingLeft=8;" vertex="1" parent="cicd-layer">
          <mxGeometry x="540" y="40" width="440" height="70" as="geometry"/>
        </mxCell>
        <mxCell id="github" value="GitHub Repo&#xa;DaTingLi/banksys_szai4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="cicd-layer">
          <mxGeometry x="1040" y="45" width="200" height="60" as="geometry"/>
        </mxCell>

        <!-- Arrows Overall -->
        <mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#6c8ebf;" edge="1" parent="1" source="browser" target="streamlit-srv">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e1-label" value=":8004 → :8501" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="e1">
          <mxGeometry x="-0.1" y="1" relative="1" as="geometry"><mxPoint as="offset"/></mxGeometry>
        </mxCell>
        <mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#82b366;" edge="1" parent="1" source="streamlit-srv" target="ui-box">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#82b366;" edge="1" parent="1" source="ui-box" target="bl-box">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#d79b00;" edge="1" parent="1" source="bl-box" target="ml-box">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#d6b656;dashed=1;" edge="1" parent="1" source="bl-box" target="train-csv">
          <mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="600" y="720"/><mxPoint x="300" y="720"/></Array></mxGeometry>
        </mxCell>
        <mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#d79b00;dashed=1;" edge="1" parent="1" source="ml-box" target="model-pkl">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#9673a6;dashed=1;" edge="1" parent="1" source="github" target="cd">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;strokeColor=#9673a6;dashed=1;" edge="1" parent="1" source="cd" target="host">
          <mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="760" y="900"/><mxPoint x="150" y="900"/></Array></mxGeometry>
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>

  <diagram id="layers" name="2-应用分层架构">
    <mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1100" math="0" shadow="0">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>

        <mxCell id="t2" value="banksys_szai4 应用分层架构（源码模块依赖关系）" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="400" y="20" width="800" height="40" as="geometry"/>
        </mxCell>

        <!-- Layer 1: UI -->
        <mxCell id="l1" value="① 表现层 — Streamlit UI（服务端渲染，前后端合一）" style="swimlane;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;startSize=30;fontSize=13;" vertex="1" parent="1">
          <mxGeometry x="80" y="80" width="1440" height="160" as="geometry"/>
        </mxCell>
        <mxCell id="main-py" value="app/main.py&#xa;首页 / 欢迎页&#xa;数据集概览指标&#xa;st.set_page_config" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="l1">
          <mxGeometry x="40" y="45" width="200" height="95" as="geometry"/>
        </mxCell>
        <mxCell id="page1" value="app/pages/01_data_analysis.py&#xa;数据分析页&#xa;st.plotly_chart 渲染图表&#xa;@st.cache_data 缓存数据" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="l1">
          <mxGeometry x="300" y="45" width="260" height="95" as="geometry"/>
        </mxCell>
        <mxCell id="page2" value="app/pages/02_prediction.py&#xa;在线预测页&#xa;st.form / selectbox / slider&#xa;调用 predict()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="l1">
          <mxGeometry x="620" y="45" width="260" height="95" as="geometry"/>
        </mxCell>
        <mxCell id="nav-note" value="页面导航：Streamlit 多页机制&#xa;app/pages/ 自动注册侧边栏" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=2" vertex="1" parent="l1">
          <mxGeometry x="940" y="55" width="280" height="75" as="geometry"/>
        </mxCell>

        <!-- Layer 2: Business -->
        <mxCell id="l2" value="② 业务逻辑层 — app/models/" style="swimlane;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;startSize=30;fontSize=13;" vertex="1" parent="1">
          <mxGeometry x="80" y="280" width="1440" height="200" as="geometry"/>
        </mxCell>
        <mxCell id="dl" value="data_loader.py&#xa;─────────────&#xa;load_train_data()&#xa;load_test_data()&#xa;get_missing_value_mask()&#xa;get_column_statistics()&#xa;─────────────&#xa;Schema 定义:&#xa;CATEGORICAL_FEATURES (10)&#xa;NUMERIC_FEATURES (10)&#xa;MODEL_FEATURES / TARGET" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=8;fontSize=11;" vertex="1" parent="l2">
          <mxGeometry x="40" y="40" width="280" height="145" as="geometry"/>
        </mxCell>
        <mxCell id="viz" value="visualizer.py&#xa;─────────────&#xa;compute_overview()&#xa;compute_subscribe_stats()&#xa;compute_categorical_stats()&#xa;compute_age_stats()&#xa;compute_numeric_stats()&#xa;─────────────&#xa;build_subscribe_pie()&#xa;build_categorical_bar()&#xa;build_age_histogram()&#xa;build_numeric_box()&#xa;─────────────&#xa;输出: Plotly Figure" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=8;fontSize=11;" vertex="1" parent="l2">
          <mxGeometry x="380" y="40" width="280" height="145" as="geometry"/>
        </mxCell>
        <mxCell id="pred" value="predictor.py&#xa;─────────────&#xa;load_model() [@lru_cache]&#xa;predict(features) → dict&#xa;  · subscribe: bool&#xa;  · probability: float&#xa;  · confidence: high/med/low&#xa;─────────────&#xa;ModelNotFoundError&#xa;读取 model.pkl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=8;fontSize=11;" vertex="1" parent="l2">
          <mxGeometry x="720" y="40" width="280" height="145" as="geometry"/>
        </mxCell>
        <mxCell id="utils" value="app/utils/&#xa;(空占位，暂无实现)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;dashed=1;" vertex="1" parent="l2">
          <mxGeometry x="1060" y="70" width="180" height="60" as="geometry"/>
        </mxCell>

        <!-- Layer 3: ML -->
        <mxCell id="l3" value="③ 机器学习层 — app/ml/" style="swimlane;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontStyle=1;startSize=30;fontSize=13;" vertex="1" parent="1">
          <mxGeometry x="80" y="520" width="1440" height="200" as="geometry"/>
        </mxCell>
        <mxCell id="train" value="train.py (离线训练 CLI)&#xa;─────────────&#xa;python -m app.ml.train&#xa;  --overwrite&#xa;  --check-auc&#xa;─────────────&#xa;build_pipeline():&#xa;  ColumnTransformer&#xa;    OneHotEncoder (cat)&#xa;    passthrough (num)&#xa;  RandomForestClassifier&#xa;    n_estimators=200&#xa;    class_weight=balanced&#xa;─────────────&#xa;80/20 stratified split&#xa;评估 AUC / Accuracy&#xa;save_model() → model.pkl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=8;fontSize=11;" vertex="1" parent="l3">
          <mxGeometry x="40" y="40" width="340" height="145" as="geometry"/>
        </mxCell>
        <mxCell id="pkl-struct" value="model.pkl 结构&#xa;─────────────&#xa;{&#xa;  pipeline: Pipeline,&#xa;  categorical_features: [...],&#xa;  numeric_features: [...],&#xa;  feature_columns: MODEL_FEATURES,&#xa;  target_positive: &quot;yes&quot;,&#xa;  random_state: 42&#xa;}" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=8;fontSize=11;fontFamily=Courier New;" vertex="1" parent="l3">
          <mxGeometry x="440" y="40" width="300" height="145" as="geometry"/>
        </mxCell>
        <mxCell id="ml-note" value="训练与推理共享 Schema&#xa;data_loader.py 为唯一真相源&#xa;ml.train 与 models.predictor 均引用" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=2;align=left;spacingLeft=8;" vertex="1" parent="l3">
          <mxGeometry x="800" y="55" width="380" height="75" as="geometry"/>
        </mxCell>

        <!-- Layer 4: Data -->
        <mxCell id="l4" value="④ 数据层 — 文件存储" style="swimlane;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1;startSize=30;fontSize=13;" vertex="1" parent="1">
          <mxGeometry x="80" y="760" width="1440" height="140" as="geometry"/>
        </mxCell>
        <mxCell id="d1" value="data/train.csv" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=12;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="l4">
          <mxGeometry x="200" y="40" width="140" height="80" as="geometry"/>
        </mxCell>
        <mxCell id="d2" value="data/test.csv" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=12;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="l4">
          <mxGeometry x="420" y="40" width="140" height="80" as="geometry"/>
        </mxCell>
        <mxCell id="d3" value="model.pkl" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=12;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="l4">
          <mxGeometry x="640" y="40" width="140" height="80" as="geometry"/>
        </mxCell>

        <!-- Dependency arrows -->
        <mxCell id="a1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;endFill=1;" edge="1" parent="1" source="main-py" target="dl">
          <mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="180" y="260"/><mxPoint x="200" y="260"/></Array></mxGeometry>
        </mxCell>
        <mxCell id="a2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;endFill=1;" edge="1" parent="1" source="page1" target="dl">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="a3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;endFill=1;" edge="1" parent="1" source="page1" target="viz">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="a4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;endFill=1;" edge="1" parent="1" source="page2" target="dl">
          <mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="750" y="260"/><mxPoint x="200" y="260"/></Array></mxGeometry>
        </mxCell>
        <mxCell id="a5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;endFill=1;" edge="1" parent="1" source="page2" target="pred">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="a6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#82b366;endArrow=block;endFill=1;" edge="1" parent="1" source="dl" target="d1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="a7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#82b366;endArrow=block;endFill=1;" edge="1" parent="1" source="dl" target="d2">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="a8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;endArrow=block;endFill=1;" edge="1" parent="1" source="train" target="dl">
          <mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="220" y="500"/><mxPoint x="180" y="500"/></Array></mxGeometry>
        </mxCell>
        <mxCell id="a9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;endArrow=block;endFill=1;" edge="1" parent="1" source="train" target="d3">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="a10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;endArrow=block;endFill=1;" edge="1" parent="1" source="pred" target="d3">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="a11" value="import" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;dashed=1;endArrow=open;endFill=0;" edge="1" parent="1" source="train" target="pred">
          <mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="500" y="620"/><mxPoint x="860" y="620"/></Array></mxGeometry>
        </mxCell>
        <mxCell id="a11l" value="共享 model.pkl" style="edgeLabel;html=1;align=center;verticalAlign=middle;" vertex="1" connectable="0" parent="a11">
          <mxGeometry relative="1" as="geometry"><mxPoint as="offset"/></mxGeometry>
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>

  <diagram id="dataflow" name="3-业务流程与数据流">
    <mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1200" math="0" shadow="0">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>

        <mxCell id="t3" value="banksys_szai4 业务流程与数据流（三条核心链路）" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="400" y="20" width="800" height="40" as="geometry"/>
        </mxCell>

        <!-- Flow 1: Analysis -->
        <mxCell id="f1-title" value="链路 A：数据分析页 (/01_data_analysis)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;startSize=30;fontSize=13;" vertex="1" parent="1">
          <mxGeometry x="60" y="80" width="1480" height="120" as="geometry"/>
        </mxCell>
        <mxCell id="f1-1" value="1. 用户打开页面" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="f1-title">
          <mxGeometry x="30" y="45" width="130" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f1-2" value="2. @st.cache_data&#xa;_get_data()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="f1-title">
          <mxGeometry x="200" y="45" width="150" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f1-3" value="3. load_train_data()&#xa;读取 train.csv" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="f1-title">
          <mxGeometry x="390" y="45" width="150" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f1-4" value="4. visualizer.compute_*()&#xa;统计计算" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="f1-title">
          <mxGeometry x="580" y="45" width="160" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f1-5" value="5. visualizer.build_*()&#xa;Plotly Figure" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="f1-title">
          <mxGeometry x="780" y="45" width="160" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f1-6" value="6. st.plotly_chart()&#xa;浏览器渲染" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="f1-title">
          <mxGeometry x="980" y="45" width="150" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f1-7" value="展示: 概览指标 / 认购分布 / 分类分析 / 年龄分析 / 数值分析" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=2" vertex="1" parent="f1-title">
          <mxGeometry x="1170" y="50" width="280" height="45" as="geometry"/>
        </mxCell>

        <!-- Flow 2: Prediction -->
        <mxCell id="f2-title" value="链路 B：在线预测页 (/02_prediction)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;startSize=30;fontSize=13;" vertex="1" parent="1">
          <mxGeometry x="60" y="240" width="1480" height="160" as="geometry"/>
        </mxCell>
        <mxCell id="f2-1" value="1. 用户打开页面" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="f2-title">
          <mxGeometry x="30" y="45" width="120" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="f2-2" value="2. _category_options()&#xa;从 train.csv 提取选项" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="f2-title">
          <mxGeometry x="180" y="40" width="170" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="f2-3" value="3. 用户填写表单&#xa;selectbox (10 分类)&#xa;slider (10 数值)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="f2-title">
          <mxGeometry x="380" y="40" width="170" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="f2-4" value="4. 点击 Predict" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="f2-title">
          <mxGeometry x="580" y="45" width="110" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="f2-5" value="5. predict(features)&#xa;load_model() [lru_cache]&#xa;读取 model.pkl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="f2-title">
          <mxGeometry x="720" y="40" width="180" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="f2-6" value="6. pipeline.predict_proba()&#xa;构建单行 DataFrame" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="f2-title">
          <mxGeometry x="930" y="40" width="190" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="f2-7" value="7. 返回结果&#xa;subscribe / probability / confidence" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="f2-title">
          <mxGeometry x="1150" y="40" width="200" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="f2-8" value="8. UI 展示预测标签 + 概率仪表 + 建议文案" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="f2-title">
          <mxGeometry x="380" y="110" width="300" height="40" as="geometry"/>
        </mxCell>

        <!-- Flow 3: Training -->
        <mxCell id="f3-title" value="链路 C：离线训练 (python -m app.ml.train)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontStyle=1;startSize=30;fontSize=13;" vertex="1" parent="1">
          <mxGeometry x="60" y="440" width="1480" height="140" as="geometry"/>
        </mxCell>
        <mxCell id="f3-1" value="1. CLI 启动&#xa;--overwrite&#xa;--check-auc" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="f3-title">
          <mxGeometry x="30" y="45" width="120" height="70" as="geometry"/>
        </mxCell>
        <mxCell id="f3-2" value="2. load_train_data()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="f3-title">
          <mxGeometry x="180" y="50" width="140" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f3-3" value="3. 80/20&#xa;stratified split" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="f3-title">
          <mxGeometry x="350" y="50" width="130" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f3-4" value="4. build_pipeline()&#xa;训练 RandomForest" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="f3-title">
          <mxGeometry x="510" y="50" width="160" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f3-5" value="5. 评估&#xa;AUC / Accuracy" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="f3-title">
          <mxGeometry x="700" y="50" width="130" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f3-6" value="6. joblib.dump&#xa;→ model.pkl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="f3-title">
          <mxGeometry x="860" y="50" width="140" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f3-7" value="Dockerfile 构建期自动执行&#xa;RUN python -m app.ml.train --overwrite" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=2" vertex="1" parent="f3-title">
          <mxGeometry x="1040" y="50" width="300" height="55" as="geometry"/>
        </mxCell>

        <!-- Flow 4: Docker Build -->
        <mxCell id="f4-title" value="链路 D：Docker 镜像构建与启动" style="swimlane;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontStyle=1;startSize=30;fontSize=13;" vertex="1" parent="1">
          <mxGeometry x="60" y="620" width="1480" height="140" as="geometry"/>
        </mxCell>
        <mxCell id="f4-1" value="FROM python:3.11-slim" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="f4-title">
          <mxGeometry x="30" y="45" width="160" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f4-2" value="pip install&#xa;requirements.txt" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="f4-title">
          <mxGeometry x="220" y="45" width="140" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f4-3" value="COPY app/ + data/" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="f4-title">
          <mxGeometry x="390" y="45" width="140" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f4-4" value="RUN train --overwrite&#xa;模型烤入镜像" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="f4-title">
          <mxGeometry x="560" y="45" width="160" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f4-5" value="EXPOSE 8501" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="f4-title">
          <mxGeometry x="750" y="45" width="110" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f4-6" value="CMD streamlit run&#xa;app/main.py" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="f4-title">
          <mxGeometry x="890" y="45" width="150" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f4-7" value="HEALTHCHECK&#xa;/_stcore/health" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="f4-title">
          <mxGeometry x="1070" y="45" width="150" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="f4-8" value="docker run -p 8004:8501" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1" vertex="1" parent="f4-title">
          <mxGeometry x="1250" y="45" width="170" height="55" as="geometry"/>
        </mxCell>

        <!-- Legend -->
        <mxCell id="legend" value="图例说明" style="swimlane;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=1;startSize=25;" vertex="1" parent="1">
          <mxGeometry x="60" y="800" width="1480" height="100" as="geometry"/>
        </mxCell>
        <mxCell id="leg1" value="黄色 = 用户/浏览器交互" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="legend">
          <mxGeometry x="30" y="35" width="200" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="leg2" value="蓝色 = Streamlit UI 层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="legend">
          <mxGeometry x="260" y="35" width="200" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="leg3" value="绿色 = 业务逻辑层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="legend">
          <mxGeometry x="490" y="35" width="200" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="leg4" value="橙色 = ML 训练/推理" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="legend">
          <mxGeometry x="720" y="35" width="200" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="leg5" value="紫色 = 部署/CI/CD" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="legend">
          <mxGeometry x="950" y="35" width="200" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="leg6" value="红色 = 健康检查/缓存" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="legend">
          <mxGeometry x="1180" y="35" width="200" height="50" as="geometry"/>
        </mxCell>

        <!-- Flow arrows within lanes -->
        <mxCell id="fa1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="f1-1" target="f1-2"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fa2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="f1-2" target="f1-3"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fa3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="f1-3" target="f1-4"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fa4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="f1-4" target="f1-5"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fa5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="f1-5" target="f1-6"><mxGeometry relative="1" as="geometry"/></mxCell>

        <mxCell id="fb1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="f2-1" target="f2-2"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fb2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="f2-2" target="f2-3"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fb3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="f2-3" target="f2-4"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fb4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="f2-4" target="f2-5"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fb5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="f2-5" target="f2-6"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fb6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="f2-6" target="f2-7"><mxGeometry relative="1" as="geometry"/></mxCell>

        <mxCell id="fc1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;endArrow=block;" edge="1" parent="1" source="f3-1" target="f3-2"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fc2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;endArrow=block;" edge="1" parent="1" source="f3-2" target="f3-3"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fc3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;endArrow=block;" edge="1" parent="1" source="f3-3" target="f3-4"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fc4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;endArrow=block;" edge="1" parent="1" source="f3-4" target="f3-5"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fc5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;endArrow=block;" edge="1" parent="1" source="f3-5" target="f3-6"><mxGeometry relative="1" as="geometry"/></mxCell>

        <mxCell id="fd1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="f4-1" target="f4-2"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fd2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="f4-2" target="f4-3"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fd3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="f4-3" target="f4-4"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fd4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="f4-4" target="f4-5"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fd5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="f4-5" target="f4-6"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fd6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="f4-6" target="f4-7"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="fd7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="f4-7" target="f4-8"><mxGeometry relative="1" as="geometry"/></mxCell>

        <!-- Cross-flow connection -->
        <mxCell id="cross1" value="model.pkl 供链路 B 使用" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d79b00;dashed=1;endArrow=open;endFill=0;" edge="1" parent="1" source="f3-6" target="f2-5">
          <mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="930" y="420"/><mxPoint x="810" y="420"/></Array></mxGeometry>
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>

  <diagram id="cicd" name="4-CI/CD与部署架构">
    <mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>

        <mxCell id="t4" value="banksys_szai4 CI/CD 与部署架构" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="350" y="20" width="700" height="40" as="geometry"/>
        </mxCell>

        <!-- Developer -->
        <mxCell id="dev" value="开发者&#xa;git push" style="shape=actor;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
          <mxGeometry x="80" y="100" width="60" height="80" as="geometry"/>
        </mxCell>

        <!-- GitHub -->
        <mxCell id="gh" value="GitHub&#xa;DaTingLi/banksys_szai4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="220" y="110" width="220" height="60" as="geometry"/>
        </mxCell>

        <!-- CI Pipeline -->
        <mxCell id="ci-box" value="CI Pipeline (.github/workflows/ci.yml)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;startSize=30;" vertex="1" parent="1">
          <mxGeometry x="500" y="80" width="820" height="200" as="geometry"/>
        </mxCell>
        <mxCell id="ci-trigger" value="触发条件&#xa;push: main, feature/**&#xa;PR → main" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="ci-box">
          <mxGeometry x="20" y="45" width="150" height="70" as="geometry"/>
        </mxCell>
        <mxCell id="ci-s1" value="checkout@v4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="ci-box">
          <mxGeometry x="200" y="55" width="110" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="ci-s2" value="Python 3.11&#xa;setup-python@v5" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="ci-box">
          <mxGeometry x="330" y="55" width="120" height="50" as="geometry"/>
        </mxCell>
        <mxCell id="ci-s3" value="pip install&#xa;requirements-dev.txt&#xa;(清华源)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="ci-box">
          <mxGeometry x="470" y="45" width="130" height="70" as="geometry"/>
        </mxCell>
        <mxCell id="ci-s4" value="ruff format --check" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="ci-box">
          <mxGeometry x="20" y="140" width="140" height="45" as="geometry"/>
        </mxCell>
        <mxCell id="ci-s5" value="ruff check" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="ci-box">
          <mxGeometry x="180" y="140" width="110" height="45" as="geometry"/>
        </mxCell>
        <mxCell id="ci-s6" value="pytest --cov=app" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="ci-box">
          <mxGeometry x="310" y="140" width="130" height="45" as="geometry"/>
        </mxCell>
        <mxCell id="ci-s7" value="docker build&#xa;banksys:test" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="ci-box">
          <mxGeometry x="460" y="140" width="130" height="45" as="geometry"/>
        </mxCell>

        <!-- CD Pipeline -->
        <mxCell id="cd-box" value="CD Pipeline (.github/workflows/cd.yml) — 仅 push main 触发" style="swimlane;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontStyle=1;startSize=30;" vertex="1" parent="1">
          <mxGeometry x="80" y="320" width="1240" height="280" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s1" value="appleboy/ssh-action&#xa;Secrets: SSH_HOST&#xa;SSH_USER&#xa;SSH_PRIVATE_KEY" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;spacingLeft=8;" vertex="1" parent="cd-box">
          <mxGeometry x="20" y="45" width="180" height="80" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s2" value="/opt/banksys&#xa;git pull / clone" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="cd-box">
          <mxGeometry x="230" y="55" width="140" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s3" value="清理旧容器&#xa;端口 8000-8010&#xa;+ banksys 容器" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="cd-box">
          <mxGeometry x="400" y="50" width="150" height="70" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s4" value="等待端口 8004 释放&#xa;(最多 15s)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="cd-box">
          <mxGeometry x="580" y="55" width="150" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s5" value="docker build&#xa;banksys:latest&#xa;(清华源)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="cd-box">
          <mxGeometry x="760" y="55" width="140" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s6" value="docker run -d&#xa;--name banksys&#xa;-p 8004:8501&#xa;--restart unless-stopped" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=8;" vertex="1" parent="cd-box">
          <mxGeometry x="930" y="45" width="180" height="80" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s7" value="sleep 5" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="cd-box">
          <mxGeometry x="20" y="160" width="100" height="45" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s8" value="curl health check&#xa;GET /_stcore/health" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="cd-box">
          <mxGeometry x="150" y="155" width="170" height="55" as="geometry"/>
        </mxCell>
        <mxCell id="cd-s9" value="部署成功&#xa;http://SSH_HOST:8004/" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1" vertex="1" parent="cd-box">
          <mxGeometry x="360" y="155" width="200" height="55" as="geometry"/>
        </mxCell>

        <!-- Production Server -->
        <mxCell id="prod" value="生产服务器" style="swimlane;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;startSize=30;" vertex="1" parent="1">
          <mxGeometry x="80" y="640" width="1240" height="160" as="geometry"/>
        </mxCell>
        <mxCell id="prod-dir" value="/opt/banksys&#xa;项目目录" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="prod">
          <mxGeometry x="40" y="45" width="140" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="prod-docker" value="Docker Engine" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="prod">
          <mxGeometry x="220" y="45" width="140" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="prod-container" value="容器 banksys&#xa;python:3.11-slim&#xa;Streamlit :8501&#xa;model.pkl 已内置" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=8;" vertex="1" parent="prod">
          <mxGeometry x="400" y="35" width="200" height="90" as="geometry"/>
        </mxCell>
        <mxCell id="prod-port" value="端口映射&#xa;8004 (host) → 8501 (container)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1" vertex="1" parent="prod">
          <mxGeometry x="640" y="45" width="220" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="prod-user" value="用户访问&#xa;http://服务器IP:8004" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="prod">
          <mxGeometry x="900" y="45" width="180" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="prod-hc" value="容器 HEALTHCHECK&#xa;每 30s 检查 /_stcore/health" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="prod">
          <mxGeometry x="1100" y="45" width="200" height="60" as="geometry"/>
        </mxCell>

        <!-- Arrows -->
        <mxCell id="c1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#666666;endArrow=block;" edge="1" parent="1" source="dev" target="gh"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="c2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="gh" target="ci-trigger"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="c3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="gh" target="cd-s1">
          <mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="330" y="300"/><mxPoint x="170" y="300"/></Array></mxGeometry>
        </mxCell>
        <mxCell id="c3l" value="push main" style="edgeLabel;html=1;align=center;" vertex="1" connectable="0" parent="c3"><mxGeometry relative="1" as="geometry"><mxPoint as="offset"/></mxGeometry></mxCell>
        <mxCell id="c4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s9" target="prod-container"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="c5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=2;strokeColor=#d6b656;endArrow=block;" edge="1" parent="1" source="prod-user" target="prod-container"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="990" y="630"/><mxPoint x="500" y="630"/></Array></mxGeometry></mxCell>

        <!-- CI internal arrows -->
        <mxCell id="ci-a1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="ci-trigger" target="ci-s1"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="ci-a2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="ci-s1" target="ci-s2"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="ci-a3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#6c8ebf;endArrow=block;" edge="1" parent="1" source="ci-s2" target="ci-s3"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="ci-a4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="ci-s3" target="ci-s4"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="535" y="200"/><mxPoint x="90" y="200"/></Array></mxGeometry></mxCell>
        <mxCell id="ci-a5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="ci-s4" target="ci-s5"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="ci-a6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#82b366;endArrow=block;" edge="1" parent="1" source="ci-s5" target="ci-s6"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="ci-a7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#d79b00;endArrow=block;" edge="1" parent="1" source="ci-s6" target="ci-s7"><mxGeometry relative="1" as="geometry"/></mxCell>

        <!-- CD internal arrows -->
        <mxCell id="cd-a1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s1" target="cd-s2"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="cd-a2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s2" target="cd-s3"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="cd-a3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s3" target="cd-s4"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="cd-a4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s4" target="cd-s5"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="cd-a5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s5" target="cd-s6"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="cd-a6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s6" target="cd-s7"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="1020" y="500"/><mxPoint x="130" y="500"/></Array></mxGeometry></mxCell>
        <mxCell id="cd-a7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s7" target="cd-s8"><mxGeometry relative="1" as="geometry"/></mxCell>
        <mxCell id="cd-a8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeWidth=1;strokeColor=#9673a6;endArrow=block;" edge="1" parent="1" source="cd-s8" target="cd-s9"><mxGeometry relative="1" as="geometry"/></mxCell>
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>
