937 lines
23 KiB
Markdown
937 lines
23 KiB
Markdown
# MES Dashboard - Architecture Findings
|
||
|
||
本文件記錄專案開發過程中確立的架構設計、全局規範與資料處理規則。
|
||
|
||
---
|
||
|
||
## 1. 資料庫連線管理
|
||
|
||
### 連線池統一使用
|
||
所有資料庫操作必須透過 `mes_dashboard.core.database` 模組:
|
||
|
||
```python
|
||
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 使用方式
|
||
|
||
```python
|
||
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 使用方式
|
||
|
||
```python
|
||
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 個值,需分批處理:
|
||
|
||
```python
|
||
BATCH_SIZE = 1000
|
||
|
||
# 參考 job_query_service.py 的 _build_resource_filter()
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 快取機制
|
||
|
||
### 多層快取架構
|
||
|
||
```
|
||
請求 → 進程級快取 (30 秒 TTL)
|
||
→ Redis 快取 (可配置 TTL)
|
||
→ Oracle 資料庫
|
||
```
|
||
|
||
### 全局快取 API
|
||
使用 `mes_dashboard.core.cache` 模組:
|
||
|
||
```python
|
||
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`:
|
||
|
||
```python
|
||
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`
|
||
|
||
### 用途
|
||
快取全站共用的篩選選項,避免重複查詢資料庫:
|
||
|
||
```python
|
||
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()` 中,自動:
|
||
- 檢查是否允許請求
|
||
- 記錄成功/失敗
|
||
- 狀態轉移
|
||
|
||
### 狀態查詢
|
||
```python
|
||
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`
|
||
|
||
### 響應格式
|
||
|
||
```python
|
||
# 成功響應
|
||
{
|
||
"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 | 內部錯誤 |
|
||
|
||
### 便利函數
|
||
|
||
```python
|
||
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 認證 (生產環境)
|
||
```python
|
||
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 管理
|
||
```python
|
||
# 登入後存入 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`
|
||
|
||
```python
|
||
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 路由控制 |
|
||
|
||
### 數據格式
|
||
```json
|
||
{
|
||
"pages": [
|
||
{"route": "/wip-overview", "name": "WIP 即時概況", "status": "released"},
|
||
{"route": "/tables", "name": "表格總覽", "status": "dev"}
|
||
],
|
||
"api_public": true
|
||
}
|
||
```
|
||
|
||
### API
|
||
|
||
```python
|
||
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
|
||
```
|
||
|
||
### 日誌記錄規範
|
||
|
||
```python
|
||
import logging
|
||
logger = logging.getLogger('mes_dashboard')
|
||
|
||
logger.debug("詳細調試資訊")
|
||
logger.info("一般操作記錄")
|
||
logger.warning("警告但可繼續")
|
||
logger.error("錯誤需要關注", exc_info=True) # 包含堆棧
|
||
```
|
||
|
||
### SQLite 日誌查詢
|
||
位置: `mes_dashboard.core.log_store`
|
||
|
||
```python
|
||
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__.py` 的 `register_routes(app)`
|
||
|
||
---
|
||
|
||
## 12. 前端全局組件
|
||
|
||
### Toast 通知
|
||
定義於 `static/js/toast.js`,透過 `_base.html` 載入:
|
||
|
||
```javascript
|
||
// 正確用法
|
||
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: 永久
|
||
|
||
### MesApi(HTTP 請求)
|
||
定義於 `static/js/mes-api.js`:
|
||
|
||
```javascript
|
||
// 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`:
|
||
|
||
```sql
|
||
((OBJECTCATEGORY = 'ASSEMBLY' AND OBJECTTYPE = 'ASSEMBLY')
|
||
OR (OBJECTCATEGORY = 'WAFERSORT' AND OBJECTTYPE = 'WAFERSORT'))
|
||
```
|
||
|
||
### 排除條件
|
||
```python
|
||
# 排除的地點
|
||
EXCLUDED_LOCATIONS = [
|
||
'ATEC', 'F區', 'F區焊接站', '報廢', '實驗室',
|
||
'山東', '成型站_F區', '焊接F區', '無錫', '熒茂'
|
||
]
|
||
|
||
# 排除的資產狀態
|
||
EXCLUDED_ASSET_STATUSES = ['Disapproved']
|
||
```
|
||
|
||
### CommonFilters 使用
|
||
位置: `mes_dashboard.sql.filters`
|
||
|
||
```python
|
||
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
|
||
```
|
||
|
||
### 狀態顯示名稱
|
||
```python
|
||
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`
|
||
|
||
```python
|
||
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
|
||
對於多個獨立查詢,使用平行執行提升效能:
|
||
|
||
```python
|
||
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 多次執行:
|
||
|
||
```sql
|
||
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 ...
|
||
```
|
||
|
||
### 日期範圍查詢
|
||
```sql
|
||
-- 包含 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. 前端資料限制
|
||
|
||
### 明細資料上限
|
||
為避免瀏覽器記憶體問題,明細查詢有筆數限制:
|
||
|
||
```python
|
||
MAX_DETAIL_RECORDS = 5000
|
||
|
||
if total > MAX_DETAIL_RECORDS:
|
||
df = df.head(MAX_DETAIL_RECORDS)
|
||
truncated = True
|
||
```
|
||
|
||
前端顯示警告:
|
||
```javascript
|
||
if (result.truncated) {
|
||
Toast.warning(`資料超過 ${result.max_records} 筆,請使用篩選條件縮小範圍。`);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 20. JavaScript 注意事項
|
||
|
||
### Array.reverse() 原地修改
|
||
```javascript
|
||
// 錯誤 - 原地修改陣列
|
||
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 # 壓力測試
|
||
```
|
||
|
||
### 測試前重置
|
||
```python
|
||
def setUp(self):
|
||
db._ENGINE = None # 重置連線池
|
||
self.app = create_app('testing')
|
||
```
|
||
|
||
### 執行測試
|
||
```bash
|
||
# 單一模組
|
||
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. 錯誤處理模式
|
||
|
||
### 三層錯誤處理
|
||
|
||
```python
|
||
# 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` |
|