Files
DashBoard/docs/architecture_findings.md
2026-02-08 08:30:48 +08:00

23 KiB
Raw Blame History

MES Dashboard - Architecture Findings

本文件記錄專案開發過程中確立的架構設計、全局規範與資料處理規則。


1. 資料庫連線管理

連線池統一使用

所有資料庫操作必須透過 mes_dashboard.core.database 模組:

from mes_dashboard.core.database import read_sql_df, get_engine

# 讀取資料 (推薦方式)
df = read_sql_df(sql, params)

# 取得 engine若需要直接操作
engine = get_engine()

連線池配置 (位置: core/database.py)

參數 開發環境 生產環境 說明
pool_size 2 10 基礎連線數
max_overflow 3 20 額外連線數
pool_timeout 30 30 等待超時 (秒)
pool_recycle 1800 1800 回收週期 (30分鐘)
pool_pre_ping True True 使用前驗證連線

Keep-Alive 機制

  • 背景執行緒每 5 分鐘執行 SELECT 1 FROM DUAL
  • 防止 NAT/防火牆斷開閒置連線
  • 啟動: start_keepalive(),停止: stop_keepalive()

注意事項

  • 禁止在各 service 中自行建立連線
  • 禁止直接使用 oracledb.connect()
  • 連線池由 database.py 統一管理,避免連線洩漏
  • 測試環境需在 setUp 中重置:db._ENGINE = None

2. SQL 集中管理

目錄結構

所有 SQL 查詢放在 src/mes_dashboard/sql/ 目錄:

sql/
├── loader.py              # SQL 檔案載入器 (LRU 快取)
├── builder.py             # 參數化查詢構建器
├── filters.py             # 通用篩選條件
├── dashboard/             # 儀表板 SQL
│   ├── kpi.sql
│   ├── heatmap.sql
│   └── workcenter_cards.sql
├── wip/                   # WIP SQL
│   ├── summary.sql
│   └── detail.sql
├── resource/              # 設備 SQL
│   ├── by_status.sql
│   └── detail.sql
├── resource_history/      # 歷史 SQL
└── job_query/             # 維修工單 SQL

SQLLoader 使用方式

from mes_dashboard.sql.loader import SQLLoader

# 載入 SQL 檔案 (自動 LRU 快取,最多 100 個)
sql = SQLLoader.load("wip/summary")

# 結構性參數替換 (用於 SQL 片段)
sql = SQLLoader.load_with_params("dashboard/kpi",
    LATEST_STATUS_SUBQUERY="...",
    WHERE_CLAUSE="...")

# 清除快取
SQLLoader.clear_cache()

QueryBuilder 使用方式

from mes_dashboard.sql.builder import QueryBuilder

builder = QueryBuilder()

# 添加條件 (自動參數化,防 SQL 注入)
builder.add_param_condition("STATUS", "PRD")
builder.add_in_condition("STATUS", ["PRD", "SBY"])
builder.add_not_in_condition("HOLD_REASON", exclude_list)
builder.add_like_condition("LOTID", user_input, position="both")
builder.add_or_like_conditions(["COL1", "COL2"], [val1, val2])
builder.add_is_null("COLUMN")
builder.add_is_not_null("COLUMN")
builder.add_condition("FIXED_CONDITION = 1")  # 固定條件

# 構建 WHERE 子句
where_clause, params = builder.build_where_only()

# 替換佔位符並執行
sql = sql.replace("{{ WHERE_CLAUSE }}", where_clause)
df = read_sql_df(sql, params)

佔位符規範

類型 語法 用途 安全性
結構性 {{ PLACEHOLDER }} 靜態 SQL 片段 僅限預定義值
參數 :param_name 動態用戶輸入 Oracle bind variables

Oracle IN 子句限制

Oracle IN 子句上限 1000 個值,需分批處理:

BATCH_SIZE = 1000

# 參考 job_query_service.py 的 _build_resource_filter()

3. 快取機制

多層快取架構

請求 → 進程級快取 (30 秒 TTL)
     → Redis 快取 (可配置 TTL)
     → Oracle 資料庫

全局快取 API

使用 mes_dashboard.core.cache 模組:

from mes_dashboard.core.cache import cache_get, cache_set, make_cache_key

# 建立快取 key支援 filters dict
cache_key = make_cache_key("resource_history_summary", filters={
    'start_date': start_date,
    'workcenter_groups': sorted(groups) if groups else None,
})

# 讀取/寫入快取
result = cache_get(cache_key)
if result is None:
    result = query_data()
    cache_set(cache_key, result, ttl=CACHE_TTL_TREND)

快取 TTL 常數

定義於 mes_dashboard.config.constants

CACHE_TTL_DEFAULT = 60           # 1 分鐘
CACHE_TTL_FILTER_OPTIONS = 600   # 10 分鐘
CACHE_TTL_PIVOT_COLUMNS = 300    # 5 分鐘
CACHE_TTL_KPI = 60               # 1 分鐘
CACHE_TTL_TREND = 300            # 5 分鐘

Redis 快取配置

環境變數:

REDIS_ENABLED=true
REDIS_URL=redis://localhost:6379/0
REDIS_KEY_PREFIX=mes_wip

專用快取服務

服務 位置 用途
WIP 快取更新器 core/cache_updater.py 背景線程自動更新 WIP 數據
資源快取 services/resource_cache.py DW_MES_RESOURCE 表快取 (4 小時同步)
設備狀態快取 services/realtime_equipment_cache.py 設備實時狀態 (5 分鐘同步)
Filter 快取 services/filter_cache.py 篩選選項快取

4. Filter Cache篩選選項快取

位置

mes_dashboard.services.filter_cache

用途

快取全站共用的篩選選項,避免重複查詢資料庫:

from mes_dashboard.services.filter_cache import (
    get_workcenter_groups,      # 取得 workcenter group 列表
    get_workcenter_mapping,     # 取得 workcentername → group 對應
    get_workcenters_for_groups, # 根據 group 取得 workcentername 列表
    get_resource_families,      # 取得 resource family 列表
)

Workcenter 對應關係

WORKCENTERNAME (資料庫)  →  WORKCENTER_GROUP (顯示)
焊接_DB_1                →  焊接_DB
焊接_DB_2                →  焊接_DB
成型_1                   →  成型

資料來源

  • Workcenter Groups: DW_PJ_LOT_V (WORKCENTER_GROUP, WORKCENTERSEQUENCE_GROUP)
  • Resource Families: DW_MES_RESOURCE (RESOURCEFAMILYNAME)

5. 熔斷器 (Circuit Breaker)

位置

mes_dashboard.core.circuit_breaker

狀態機制

CLOSED (正常)
    ↓ 失敗達到閾值
OPEN (故障,拒絕請求)
    ↓ 等待 recovery_timeout
HALF_OPEN (測試恢復)
    ↓ 成功 → CLOSED / 失敗 → OPEN

配置 (環境變數)

CIRCUIT_BREAKER_ENABLED=true
CIRCUIT_BREAKER_FAILURE_THRESHOLD=5      # 最少失敗次數
CIRCUIT_BREAKER_FAILURE_RATE=0.5         # 失敗率閾值 (0.0-1.0)
CIRCUIT_BREAKER_RECOVERY_TIMEOUT=30      # OPEN 狀態等待秒數
CIRCUIT_BREAKER_WINDOW_SIZE=10           # 滑動窗口大小

使用方式

熔斷器已整合在 read_sql_df() 中,自動:

  • 檢查是否允許請求
  • 記錄成功/失敗
  • 狀態轉移

狀態查詢

from mes_dashboard.core.circuit_breaker import get_database_circuit_breaker

cb = get_database_circuit_breaker()
status = cb.get_status()
# status.state, status.failure_count, status.success_count, status.failure_rate

6. 統一 API 響應格式

位置

mes_dashboard.core.response

響應格式

# 成功響應
{
    "success": True,
    "data": {...},
    "meta": {"timestamp": "2024-02-04T10:30:45.123456"}
}

# 錯誤響應
{
    "success": False,
    "error": {
        "code": "DB_CONNECTION_FAILED",
        "message": "資料庫連線失敗,請稍後再試",
        "details": "ORA-12541"  # 僅開發模式
    },
    "meta": {"timestamp": "..."}
}

錯誤代碼

代碼 HTTP 說明
DB_CONNECTION_FAILED 503 資料庫連線失敗
DB_QUERY_TIMEOUT 504 查詢逾時
DB_QUERY_ERROR 500 查詢執行錯誤
SERVICE_UNAVAILABLE 503 服務不可用
CIRCUIT_BREAKER_OPEN 503 熔斷器開啟
VALIDATION_ERROR 400 驗證失敗
UNAUTHORIZED 401 未授權
FORBIDDEN 403 禁止訪問
NOT_FOUND 404 不存在
TOO_MANY_REQUESTS 429 過多請求
INTERNAL_ERROR 500 內部錯誤

便利函數

from mes_dashboard.core.response import (
    success_response,
    validation_error,      # 400
    unauthorized_error,    # 401
    forbidden_error,       # 403
    not_found_error,       # 404
    db_connection_error,   # 503
    internal_error,        # 500
)

7. 認證與授權機制

認證服務

位置: mes_dashboard.services.auth_service

LDAP 認證 (生產環境)

from mes_dashboard.services.auth_service import authenticate

user = authenticate(username, password)
# 返回: {username, displayName, mail, department}

本地認證 (開發環境)

LOCAL_AUTH_ENABLED=true
LOCAL_AUTH_USERNAME=admin
LOCAL_AUTH_PASSWORD=password

Session 管理

# 登入後存入 session
session["admin"] = {
    "username": user.get("username"),
    "displayName": user.get("displayName"),
    "mail": user.get("mail"),
    "department": user.get("department"),
    "login_time": datetime.now().isoformat()
}

# Session 配置
SESSION_COOKIE_SECURE = True     # HTTPS only (生產)
SESSION_COOKIE_HTTPONLY = True   # 防止 JS 訪問
SESSION_COOKIE_SAMESITE = 'Lax'  # CSRF 防護
PERMANENT_SESSION_LIFETIME = 28800  # 8 小時

權限檢查

位置: mes_dashboard.core.permissions

from mes_dashboard.core.permissions import is_admin_logged_in, admin_required

# 檢查登入狀態
if is_admin_logged_in():
    ...

# 裝飾器保護路由
@admin_required
def admin_only_view():
    ...

登入速率限制

  • 單 IP 每 5 分鐘最多 5 次嘗試
  • 位置: routes/auth_routes.py

8. 頁面狀態管理

位置

  • 服務: mes_dashboard.services.page_registry
  • 數據: data/page_status.json

狀態定義

狀態 說明
released 所有用戶可訪問
dev 僅管理員可訪問
None 未註冊,由 Flask 路由控制

數據格式

{
  "pages": [
    {"route": "/wip-overview", "name": "WIP 即時概況", "status": "released"},
    {"route": "/tables", "name": "表格總覽", "status": "dev"}
  ],
  "api_public": true
}

API

from mes_dashboard.services.page_registry import (
    get_page_status,    # 取得頁面狀態
    set_page_status,    # 設定頁面狀態
    is_api_public,      # API 是否公開
    get_all_pages,      # 取得所有頁面
)

權限檢查 (自動)

app.py@app.before_request 中自動執行:

  • dev 頁面 + 非管理員 → 403

9. 日誌系統

雙層日誌架構

層級 目標 用途
控制台 (stderr) Gunicorn 捕獲 即時監控
SQLite 管理員儀表板 歷史查詢

配置 (環境變數)

LOG_STORE_ENABLED=true
LOG_SQLITE_PATH=logs/admin_logs.sqlite
LOG_SQLITE_RETENTION_DAYS=7
LOG_SQLITE_MAX_ROWS=100000

日誌記錄規範

import logging
logger = logging.getLogger('mes_dashboard')

logger.debug("詳細調試資訊")
logger.info("一般操作記錄")
logger.warning("警告但可繼續")
logger.error("錯誤需要關注", exc_info=True)  # 包含堆棧

SQLite 日誌查詢

位置: mes_dashboard.core.log_store

from mes_dashboard.core.log_store import get_log_store

store = get_log_store()
logs = store.query_logs(
    level="ERROR",
    limit=100,
    offset=0,
    search="keyword"
)

10. 健康檢查

端點

端點 認證 說明
/health 無需 基本健康檢查
/health/deep 需管理員 詳細指標

基本檢查項目

  • 資料庫連線 (SELECT 1 FROM DUAL)
  • Redis 連線 (PING)
  • 各快取狀態

詳細檢查項目 (deep)

  • 資料庫延遲 (毫秒)
  • 連線池狀態 (size, checked_out, overflow)
  • 快取新鮮度
  • 熔斷器狀態
  • 查詢性能指標 (P50/P95/P99)

狀態判定

  • 200 OK (healthy/degraded): DB 正常
  • 503 Unavailable (unhealthy): DB 故障

11. API 路由結構 (Blueprint)

Blueprint 列表

Blueprint URL 前綴 檔案
wip /api/wip wip_routes.py
resource /api/resource resource_routes.py
dashboard /api/dashboard dashboard_routes.py
excel_query /api/excel-query excel_query_routes.py
hold /api/hold hold_routes.py
resource_history /api/resource-history resource_history_routes.py
job_query /api/job-query job_query_routes.py
admin /admin admin_routes.py
auth /admin auth_routes.py
health / health_routes.py

路由註冊

位置: routes/__init__.pyregister_routes(app)


12. 前端全局組件

Toast 通知

定義於 static/js/toast.js,透過 _base.html 載入:

// 正確用法
Toast.info('訊息');
Toast.success('成功');
Toast.warning('警告');
Toast.error('錯誤', { retry: () => loadData() });

const id = Toast.loading('載入中...');
Toast.update(id, { message: '完成!' });
Toast.dismiss(id);

// 錯誤用法(不存在)
MESToast.warning('...');  // ❌ 錯誤

自動消失時間

  • info: 3000ms
  • success: 2000ms
  • warning: 5000ms
  • error: 永久(需手動關閉)
  • loading: 永久

MesApiHTTP 請求)

定義於 static/js/mes-api.js

// GET 請求
const data = await MesApi.get('/api/wip/summary', {
    params: { page: 1 },
    timeout: 60000,
    retries: 5,
    signal: abortController.signal,
    silent: true  // 禁用 toast 通知
});

// POST 請求
const data = await MesApi.post('/api/query_table', {
    table_name: 'TABLE_A',
    filters: {...}
});

MesApi 特性

  • 自動重試 (3 次,指數退避: 1s, 2s, 4s)
  • 自動 Toast 通知
  • 請求 ID 追蹤
  • AbortSignal 支援
  • 4xx 不重試5xx 重試

13. 資料表預篩選規則

設備類型篩選

定義於 mes_dashboard.config.constants.EQUIPMENT_TYPE_FILTER

((OBJECTCATEGORY = 'ASSEMBLY' AND OBJECTTYPE = 'ASSEMBLY')
 OR (OBJECTCATEGORY = 'WAFERSORT' AND OBJECTTYPE = 'WAFERSORT'))

排除條件

# 排除的地點
EXCLUDED_LOCATIONS = [
    'ATEC', 'F區', 'F區焊接站', '報廢', '實驗室',
    '山東', '成型站_F區', '焊接F區', '無錫', '熒茂'
]

# 排除的資產狀態
EXCLUDED_ASSET_STATUSES = ['Disapproved']

CommonFilters 使用

位置: mes_dashboard.sql.filters

from mes_dashboard.sql.filters import CommonFilters

# 添加標準篩選
CommonFilters.add_location_exclusion(builder, 'r')
CommonFilters.add_asset_status_exclusion(builder, 'r')
CommonFilters.add_wip_base_filters(builder, filters)
CommonFilters.add_equipment_filter(builder, filters)

14. 資料庫欄位對應

DW_MES_RESOURCE

常見錯誤 正確欄位名
ASSETSTATUS PJ_ASSETSSTATUS雙 S
LOCATION LOCATIONNAME
ISPRODUCTION PJ_ISPRODUCTION
ISKEY PJ_ISKEY
ISMONITOR PJ_ISMONITOR

DW_MES_RESOURCESTATUS_SHIFT

欄位 說明
HISTORYID 對應 DW_MES_RESOURCE.RESOURCEID
TXNDATE 交易日期
OLDSTATUSNAME E10 狀態 (PRD, SBY, UDT, SDT, EGT, NST)
HOURS 該狀態時數

DW_PJ_LOT_V

欄位 說明
WORKCENTERNAME 站點名稱(細分)
WORKCENTER_GROUP 站點群組(顯示用)
WORKCENTERSEQUENCE_GROUP 群組排序

15. E10 狀態定義

狀態 說明 計入 OU%
PRD Production生產 是(分子)
SBY Standby待機 是(分母)
UDT Unscheduled Downtime非計畫停機 是(分母)
SDT Scheduled Downtime計畫停機 是(分母)
EGT Engineering Time工程時間 是(分母)
NST Non-Scheduled Time非排程時間

OU% 計算公式

OU% = PRD / (PRD + SBY + UDT + SDT + EGT) × 100

狀態顯示名稱

STATUS_DISPLAY_NAMES = {
    'PRD': '生產中',
    'SBY': '待機',
    'UDT': '非計畫停機',
    'SDT': '計畫停機',
    'EGT': '工程時間',
    'NST': '未排單',
}

16. 配置管理

環境變數 (.env)

資料庫

DB_HOST=<your_database_host>
DB_PORT=1521
DB_SERVICE=<your_service_name>
DB_USER=<your_username>
DB_PASSWORD=<your_password>
DB_POOL_SIZE=5
DB_MAX_OVERFLOW=10

實際值請參考 .env.env.example

Flask

FLASK_ENV=production
FLASK_DEBUG=0
SECRET_KEY=your_secret_key
SESSION_LIFETIME=28800

認證

LDAP_API_URL=<your_ldap_api_url>
ADMIN_EMAILS=<admin_email_list>
LOCAL_AUTH_ENABLED=false

Gunicorn

GUNICORN_BIND=0.0.0.0:8080
GUNICORN_WORKERS=4
GUNICORN_THREADS=8

快取

REDIS_ENABLED=true
REDIS_URL=redis://localhost:6379/0
CACHE_CHECK_INTERVAL=600
RESOURCE_CACHE_ENABLED=true
RESOURCE_SYNC_INTERVAL=14400

熔斷器

CIRCUIT_BREAKER_ENABLED=true
CIRCUIT_BREAKER_FAILURE_THRESHOLD=5
CIRCUIT_BREAKER_FAILURE_RATE=0.5
CIRCUIT_BREAKER_RECOVERY_TIMEOUT=30

日誌

LOG_STORE_ENABLED=true
LOG_SQLITE_PATH=logs/admin_logs.sqlite
LOG_SQLITE_RETENTION_DAYS=7

環境配置類

位置: mes_dashboard.config.settings

class DevelopmentConfig(Config):
    DEBUG = True
    DB_POOL_SIZE = 2

class ProductionConfig(Config):
    DEBUG = False
    DB_POOL_SIZE = 10

class TestingConfig(Config):
    TESTING = True
    DB_POOL_SIZE = 1

17. 平行查詢

ThreadPoolExecutor

對於多個獨立查詢,使用平行執行提升效能:

from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=4) as executor:
    futures = {
        executor.submit(read_sql_df, kpi_sql): 'kpi',
        executor.submit(read_sql_df, trend_sql): 'trend',
        executor.submit(read_sql_df, heatmap_sql): 'heatmap',
    }
    for future in as_completed(futures):
        query_name = futures[future]
        results[query_name] = future.result()

注意事項

  • Mock 測試時不能使用 side_effect 列表(順序不可預測)
  • 應使用函式判斷 SQL 內容來回傳對應的 mock 資料

18. Oracle SQL 優化

CTE MATERIALIZE Hint

防止 Oracle 優化器將 CTE inline 多次執行:

WITH shift_data AS (
    SELECT /*+ MATERIALIZE */ HISTORYID, TXNDATE, OLDSTATUSNAME, HOURS
    FROM DW_MES_RESOURCESTATUS_SHIFT
    WHERE TXNDATE >= TO_DATE('2024-01-01', 'YYYY-MM-DD')
      AND TXNDATE < TO_DATE('2024-01-07', 'YYYY-MM-DD') + 1
)
SELECT ...

日期範圍查詢

-- 包含 end_date 當天
WHERE TXNDATE >= TO_DATE(:start_date, 'YYYY-MM-DD')
  AND TXNDATE < TO_DATE(:end_date, 'YYYY-MM-DD') + 1

慢查詢警告

  • 閾值: 1 秒 (警告)5 秒 (SLOW_QUERY_THRESHOLD)
  • 自動記錄到日誌

19. 前端資料限制

明細資料上限

為避免瀏覽器記憶體問題,明細查詢有筆數限制:

MAX_DETAIL_RECORDS = 5000

if total > MAX_DETAIL_RECORDS:
    df = df.head(MAX_DETAIL_RECORDS)
    truncated = True

前端顯示警告:

if (result.truncated) {
    Toast.warning(`資料超過 ${result.max_records} 筆,請使用篩選條件縮小範圍。`);
}

20. JavaScript 注意事項

Array.reverse() 原地修改

// 錯誤 - 原地修改陣列
const arr = [1, 2, 3];
arr.reverse();  // arr 被修改為 [3, 2, 1]

// 正確 - 建立新陣列
const reversed = arr.slice().reverse();  // arr 不變
// 或
const reversed = [...arr].reverse();

21. 測試規範

測試檔案結構

tests/
├── conftest.py              # pytest fixtures
├── test_*_service.py        # 單元測試service layer
├── test_*_routes.py         # 整合測試API endpoints
├── e2e/
│   └── test_*_e2e.py        # 端對端測試(完整流程)
└── stress/
    └── test_*.py            # 壓力測試

測試前重置

def setUp(self):
    db._ENGINE = None  # 重置連線池
    self.app = create_app('testing')

執行測試

# 單一模組
pytest tests/test_resource_history_service.py -v

# 全部相關測試
pytest tests/test_resource_history_*.py tests/e2e/test_resource_history_e2e.py -v

# 覆蓋率報告
pytest tests/ --cov=mes_dashboard

22. 錯誤處理模式

三層錯誤處理

# 1. 路由層 - 驗證錯誤
@bp.route('/api/query')
def query():
    if not request.json.get('table_name'):
        return validation_error("table_name 為必填")

# 2. 服務層 - 業務錯誤 (優雅降級)
def get_wip_summary(filters):
    try:
        df = query_wip(filters)
        if df.empty:
            return None
        return process_data(df)
    except Exception as exc:
        logger.error(f"WIP query failed: {exc}")
        return None

# 3. 核心層 - 基礎設施錯誤
def read_sql_df(sql, params):
    if not circuit_breaker.allow_request():
        raise RuntimeError("Circuit breaker open")

全局錯誤處理

位置: app.py_register_error_handlers()

  • 401 → unauthorized_error()
  • 403 → forbidden_error()
  • 404 → JSON (API) 或 HTML (頁面)
  • 500 → internal_error()
  • Exception → 通用處理

參考檔案索引

功能 檔案位置
SQL 載入 src/mes_dashboard/sql/loader.py
查詢構建 src/mes_dashboard/sql/builder.py
通用篩選 src/mes_dashboard/sql/filters.py
資料庫操作 src/mes_dashboard/core/database.py
快取 src/mes_dashboard/core/cache.py
熔斷器 src/mes_dashboard/core/circuit_breaker.py
API 響應 src/mes_dashboard/core/response.py
權限檢查 src/mes_dashboard/core/permissions.py
日誌存儲 src/mes_dashboard/core/log_store.py
配置類 src/mes_dashboard/config/settings.py
常量定義 src/mes_dashboard/config/constants.py
認證服務 src/mes_dashboard/services/auth_service.py
頁面狀態 src/mes_dashboard/services/page_registry.py
Filter 快取 src/mes_dashboard/services/filter_cache.py
資源快取 src/mes_dashboard/services/resource_cache.py
API 客戶端 src/mes_dashboard/static/js/mes-api.js
Toast 系統 src/mes_dashboard/static/js/toast.js