docs: 更新專案開發指南與架構文檔

- 將 claude.md 重命名為 CLAUDE.md (符合官方慣例)
- 大幅擴充 architecture_findings.md (311→935 行)
- 新增章節: SQL 集中管理、熔斷器、API 響應格式、認證授權、
  頁面狀態管理、日誌系統、健康檢查、Blueprint 結構、配置管理、
  錯誤處理模式
- 修正 MesApi 位置 (api.js → mes-api.js)
- 移除敏感資訊,改用 placeholder 格式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-02-04 17:57:22 +08:00
parent 097f72f8b9
commit 4a470fb6a6
3 changed files with 1286 additions and 101 deletions

617
CLAUDE.md Normal file
View File

@@ -0,0 +1,617 @@
# MES Dashboard 開發指南
> 此文檔為 AI 開發助手的專案開發規範,確保後續開發保持一致性。
## 專案概述
MES Dashboard 是一個**製造執行系統儀表板**,用於即時監控在製品 (WIP)、設備狀態、生產效率等 MES/ERP 數據。
- **技術棧**: Flask 3.0 + SQLAlchemy 2.0 + Oracle (oracledb) + Redis + Jinja2 + Vue 3
- **Python 版本**: 3.9+(推薦 3.11
- **部署**: Gunicorn + Systemd
---
## 開始任務前
1. **閱讀架構文檔**: 查看 `docs/architecture_findings.md` 了解:
- 資料庫連線管理模式
- 快取機制和 TTL 常量
- Filter cache (workcenter/family) 使用方式
- 前端全局組件 (Toast, MesApi)
- 數據表篩選規則和欄位映射
- E10 狀態定義和 OU% 計算
- 測試慣例
2. **修改後更新文檔**: 如果修改了以下模式,需更新 `docs/architecture_findings.md`:
- 資料庫連線或連線池方式
- 快取策略或 TTL 值
- 全局前端組件使用
- 數據表欄位名稱或篩選規則
- 新的共用工具或服務
- 測試慣例或設置模式
---
## 目錄結構
```
/home/egg/Project/DashBoard/
├── src/mes_dashboard/ # 主應用程式
│ ├── config/ # 配置管理
│ │ ├── settings.py # Flask 環境配置類
│ │ ├── database.py # 資料庫連線配置
│ │ ├── constants.py # 應用常量 (狀態碼、TTL、排除條件)
│ │ ├── tables.py # 表格元數據配置
│ │ └── workcenter_groups.py
│ ├── core/ # 核心基礎設施
│ │ ├── database.py # 連線池、查詢執行、keep-alive
│ │ ├── cache.py # 快取抽象層
│ │ ├── cache_updater.py # WIP 快取背景更新
│ │ ├── circuit_breaker.py # 熔斷器保護
│ │ ├── redis_client.py # Redis 客戶端
│ │ ├── response.py # API 響應標準化
│ │ ├── permissions.py # 權限檢查
│ │ ├── log_store.py # SQLite 日誌儲存
│ │ ├── metrics.py # 性能指標收集
│ │ └── utils.py # 通用工具函數
│ ├── routes/ # API 路由 (Blueprint)
│ ├── services/ # 業務邏輯層
│ ├── sql/ # SQL 查詢模板 (集中管理)
│ │ ├── loader.py # SQL 檔案載入器
│ │ ├── builder.py # 參數化查詢構建器
│ │ ├── filters.py # 通用篩選條件
│ │ ├── dashboard/ # 儀表板 SQL
│ │ ├── wip/ # WIP SQL
│ │ ├── resource/ # 設備 SQL
│ │ ├── resource_history/ # 歷史 SQL
│ │ └── job_query/ # 維修工單 SQL
│ ├── templates/ # Jinja2 HTML 模板
│ └── static/ # 靜態資源 (JS/CSS/dist)
├── tests/ # 測試套件
├── data/ # 運行時數據 (page_status.json)
├── logs/ # 日誌目錄
├── .env # 環境變數 (不提交版控)
└── requirements.txt # Python 依賴
```
---
## SQL 管理規範
### 原則
1. **SQL 檔案集中管理**: 所有 SQL 查詢放在 `src/mes_dashboard/sql/` 目錄下
2. **禁止硬編碼 SQL**: 不在 Python 程式碼中直接寫入複雜 SQL
3. **使用 QueryBuilder**: 所有動態條件必須使用 `QueryBuilder` 構建
### SQL 檔案位置規範
```
sql/
├── dashboard/ # 儀表板相關 (kpi.sql, heatmap.sql, ...)
├── wip/ # WIP 相關 (summary.sql, detail.sql, ...)
├── resource/ # 設備相關 (by_status.sql, detail.sql, ...)
├── resource_history/ # 歷史相關
└── job_query/ # 維修工單相關
```
### SQL 載入方式
```python
from mes_dashboard.sql.loader import SQLLoader
# 載入 SQL 檔案 (自動 LRU 快取)
sql = SQLLoader.load("wip/summary")
# 結構性參數替換 (用於 SQL 片段)
sql = SQLLoader.load_with_params("dashboard/kpi",
LATEST_STATUS_SUBQUERY="...")
```
### QueryBuilder 使用規範
```python
from mes_dashboard.sql.builder import QueryBuilder
builder = QueryBuilder()
# 添加條件 (自動參數化,防 SQL 注入)
builder.add_in_condition("STATUS", ["PRD", "SBY"])
builder.add_like_condition("LOTID", user_input, position="both")
builder.add_not_in_condition("HOLD_REASON", exclude_list)
# 構建 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` | 動態用戶輸入 | Bind variables |
### Oracle SQL 特殊規則
```sql
-- CTE 多次使用時加 MATERIALIZE 提示
WITH cte AS (/*+ MATERIALIZE */ SELECT ...)
-- 日期範圍查詢
TXNDATE >= :start_date AND TXNDATE < :end_date + 1
-- IN 子句上限 1000 個值,需分批處理
```
---
## 資料庫連線規範
### 連線池設置 (位置: `core/database.py`)
```python
# 生產環境配置
QueuePool(
pool_size=10, # 基礎連線數
max_overflow=20, # 額外連線數 (總計 30)
pool_timeout=30, # 等待超時 30 秒
pool_recycle=1800, # 30 分鐘回收
pool_pre_ping=True, # 使用前驗證
)
# 開發環境配置
pool_size=2, max_overflow=3
```
### 查詢執行規範
```python
from mes_dashboard.core.database import read_sql_df
# 標準查詢執行
df = read_sql_df(sql, params)
# 自動功能:
# - Circuit Breaker 檢查
# - 慢查詢警告 (>1 秒)
# - 指標記錄
# - 連線自動歸還
```
### 禁止事項
- ❌ 直接使用 `oracledb.connect()` 建立連線
- ❌ 在 Service 層手動管理連線
- ❌ 忘記使用 `with` 語句或 `g.db`
- ❌ 在 SQL 字串中拼接用戶輸入
---
## 資料表欄位規則
### 關鍵欄位映射
| 表名 | 正確欄位 | 錯誤欄位 |
|------|---------|---------|
| DW_MES_RESOURCE | `PJ_ASSETSSTATUS` | ~~ASSETSTATUS~~ |
| DW_MES_RESOURCE | `LOCATIONNAME` | ~~LOCATION~~ |
| DW_MES_RESOURCESTATUS_SHIFT | `HISTORYID` (映射到 RESOURCEID) | |
| DW_PJ_LOT_V | `WORKCENTER_GROUP` 映射來源 | |
### 標準篩選條件
```python
# 位置排除 (config/constants.py)
EXCLUDED_LOCATIONS = ['ATEC', 'F區', '報廢', '實驗室', ...]
# 資產狀態排除
EXCLUDED_ASSET_STATUSES = ['Disapproved']
# 設備類型篩選
EQUIPMENT_TYPE_FILTER = """
((OBJECTCATEGORY = 'ASSEMBLY' AND OBJECTTYPE = 'ASSEMBLY')
OR (OBJECTCATEGORY = 'WAFERSORT' AND OBJECTTYPE = 'WAFERSORT'))
"""
```
---
## API 設計規範
### Blueprint 結構
```python
# 路由檔案位置: routes/<module>_routes.py
# URL 前綴: /api/<module>/
wip_bp = Blueprint('wip', __name__, url_prefix='/api/wip')
resource_bp = Blueprint('resource', __name__, url_prefix='/api/resource')
```
### 統一響應格式 (位置: `core/response.py`)
```python
# 成功響應
{
"success": True,
"data": {...},
"meta": {"timestamp": "ISO8601"}
}
# 錯誤響應
{
"success": False,
"error": {
"code": "DB_CONNECTION_FAILED",
"message": "資料庫連線失敗,請稍後再試",
"details": "ORA-12541" # 僅開發模式
}
}
```
### 錯誤代碼規範
```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
)
```
---
## 前端開發規範
### 範本繼承
```jinja2
{# 頁面模板必須繼承 _base.html #}
{% extends "_base.html" %}
{% block head_extra %}
{# 頁面特定的 CSS #}
{% endblock %}
{% block content %}
{# 頁面內容 #}
{% endblock %}
{% block scripts %}
{# 頁面特定的 JS #}
{% endblock %}
```
### API 調用規範
使用統一的 API 客戶端 (`static/js/mes-api.js`)
```javascript
// GET 請求
const data = await MesApi.get('/api/wip/summary', {
params: { page: 1 },
timeout: 60000,
});
// POST 請求
const data = await MesApi.post('/api/query_table', {
table_name: 'TABLE_A',
filters: {...}
});
// 特性:
// - 自動重試 (3 次,指數退避)
// - 自動 Toast 通知
// - 請求 ID 追蹤
// - AbortSignal 支援
```
### Toast 通知規範
```javascript
// 使用全局 Toast 系統 (NOT MESToast)
Toast.info('訊息內容');
Toast.success('操作成功');
Toast.warning('警告訊息');
Toast.error('連線失敗', { retry: () => loadData() });
const id = Toast.loading('載入中...');
Toast.dismiss(id);
```
### JavaScript 注意事項
```javascript
// .reverse() 會修改原陣列,使用前先複製
const reversed = [...originalArray].reverse();
```
---
## 快取策略
### 多層快取架構
```
請求 → 進程級快取 (30 秒 TTL)
→ Redis 快取 (可配置 TTL)
→ Oracle 資料庫
```
### 快取 TTL 常量 (位置: `config/constants.py`)
```python
CACHE_TTL_DEFAULT = 60 # 1 分鐘
CACHE_TTL_FILTER_OPTIONS = 600 # 10 分鐘
CACHE_TTL_KPI = 60 # 1 分鐘
```
### 快取使用方式
```python
# 通用快取
from mes_dashboard.core.cache import cache_get, cache_set
# Filter 快取 (workcenter/family)
from mes_dashboard.services.filter_cache import get_workcenters
# 重要: WORKCENTERNAME → WORKCENTER_GROUP 轉換
```
---
## 配置管理規範
### 環境變數 (.env)
```bash
# 資料庫 (必需) - 實際值請參考 .env.example
DB_HOST=<your_database_host>
DB_PORT=1521
DB_SERVICE=<your_service_name>
DB_USER=<your_username>
DB_PASSWORD=<your_password>
# Flask (必需)
FLASK_ENV=production
SECRET_KEY=<your_secret_key>
# 認證 (必需)
LDAP_API_URL=<your_ldap_api_url>
ADMIN_EMAILS=<admin_email_list>
# 快取 (建議)
REDIS_ENABLED=true
REDIS_URL=redis://localhost:6379/0
# 熔斷器 (生產建議啟用)
CIRCUIT_BREAKER_ENABLED=true
CIRCUIT_BREAKER_FAILURE_THRESHOLD=5
```
### 環境配置類 (位置: `config/settings.py`)
```python
# 根據 FLASK_ENV 自動選擇配置
class DevelopmentConfig(Config):
DEBUG = True
DB_POOL_SIZE = 2
class ProductionConfig(Config):
DEBUG = False
DB_POOL_SIZE = 10
```
### 禁止事項
- ❌ 硬編碼敏感資訊 (密碼、密鑰)
- ❌ 提交 `.env` 檔案到版控
- ❌ 在程式碼中直接寫死配置值
---
## 錯誤處理規範
### 三層錯誤處理
```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 # 返回 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")
# ... 執行查詢
```
### 熔斷器狀態
```
CLOSED → 正常運作
OPEN → 拒絕請求 (達到失敗閾值)
HALF_OPEN → 測試恢復
```
### 日誌記錄規範
```python
import logging
logger = logging.getLogger('mes_dashboard')
# 層級使用:
logger.debug("詳細調試資訊")
logger.info("一般操作記錄")
logger.warning("警告但可繼續")
logger.error("錯誤需要關注", exc_info=True) # 包含堆棧
```
---
## 頁面狀態管理
### 頁面狀態 (位置: `data/page_status.json`)
```json
{
"pages": [
{"route": "/wip-overview", "name": "WIP 即時概況", "status": "released"},
{"route": "/tables", "name": "表格總覽", "status": "dev"}
],
"api_public": true
}
```
### 狀態定義
- `released`: 所有用戶可訪問
- `dev`: 僅管理員可訪問
- `None`: 未註冊,由 Flask 路由控制
---
## 測試規範
### 測試目錄結構
```
tests/
├── conftest.py # pytest fixtures
├── test_*_service.py # 服務層測試 (單元測試)
├── test_*_routes.py # API 端點測試 (整合測試)
├── e2e/test_*_e2e.py # 端對端測試
└── stress/ # 壓力測試
```
### 測試注意事項
```python
# 在 setUp 中重置資料庫引擎
from mes_dashboard.core import database as db
db._ENGINE = None
# 並行查詢 mock (ThreadPoolExecutor)
# 使用 function-based side_effect不用 list
mock_read_sql.side_effect = lambda sql, params: ...
```
### 執行測試
```bash
# 單元測試
pytest tests/ -v
# 特定測試
pytest tests/test_wip_service.py -v
# 覆蓋率報告
pytest tests/ --cov=mes_dashboard
```
---
## 常見反模式
### ❌ 避免的做法
```python
# 1. SQL 字串拼接 (SQL 注入風險)
sql = f"SELECT * FROM TABLE WHERE ID = '{user_input}'"
# 2. 直接建立資料庫連線
conn = oracledb.connect(...)
# 3. 硬編碼配置
DB_HOST = "192.168.1.100" # 不要這樣做!
# 4. 忽略錯誤
try:
do_something()
except:
pass
# 5. 在路由中直接執行 SQL
@bp.route('/api/data')
def get_data():
sql = "SELECT * FROM TABLE"
df = pd.read_sql(sql, conn) # 錯誤!
```
### ✅ 正確做法
```python
# 1. 使用 QueryBuilder
builder = QueryBuilder()
builder.add_param_condition("ID", user_input)
# 2. 使用 read_sql_df()
df = read_sql_df(sql, params)
# 3. 使用環境變數
DB_HOST = os.getenv('DB_HOST')
# 4. 記錄錯誤
try:
do_something()
except Exception as exc:
logger.error(f"Failed: {exc}")
return error_response(...)
# 5. 分層架構
# routes → services → core/database
```
---
## 新功能開發檢查清單
- [ ] SQL 查詢放在 `sql/` 目錄
- [ ] 使用 `QueryBuilder` 構建動態條件
- [ ] 使用 `read_sql_df()` 執行查詢
- [ ] API 響應使用標準格式
- [ ] 錯誤處理有日誌記錄
- [ ] 敏感配置使用環境變數
- [ ] 有對應的單元測試
- [ ] 頁面註冊到 `page_status.json`
- [ ] 更新 `docs/architecture_findings.md` (如有架構變更)
---
## 參考檔案
| 功能 | 檔案位置 |
|------|---------|
| SQL 載入 | `src/mes_dashboard/sql/loader.py` |
| 查詢構建 | `src/mes_dashboard/sql/builder.py` |
| 資料庫操作 | `src/mes_dashboard/core/database.py` |
| API 響應 | `src/mes_dashboard/core/response.py` |
| 熔斷器 | `src/mes_dashboard/core/circuit_breaker.py` |
| 配置類 | `src/mes_dashboard/config/settings.py` |
| 常量定義 | `src/mes_dashboard/config/constants.py` |
| 頁面狀態 | `src/mes_dashboard/services/page_registry.py` |
| API 客戶端 | `src/mes_dashboard/static/js/mes-api.js` |
| Toast 系統 | `src/mes_dashboard/static/js/toast.js` |
| 架構文檔 | `docs/architecture_findings.md` |

View File

@@ -1,58 +0,0 @@
# MES Dashboard - Claude Code Instructions
## Project Overview
MES Dashboard 是一個工廠製造執行系統的儀表板應用,使用 Flask + Oracle 資料庫 + ECharts 前端圖表。
## Before Starting Any Task
1. **Review architecture documentation**: Read `docs/architecture_findings.md` to understand:
- Database connection management patterns
- Caching mechanisms and TTL constants
- Filter cache (workcenter/family) usage
- Frontend global components (Toast, MesApi)
- Data table filtering rules and column mappings
- E10 status definitions and OU% calculation
- Testing conventions
## When Making Changes
If any of the following patterns are modified or new patterns are discovered:
- Database connection or pooling approach
- Caching strategy or TTL values
- Global frontend components usage
- Data table column names or filtering rules
- New shared utilities or services
- Testing conventions or setup patterns
**Update `docs/architecture_findings.md`** to reflect the changes.
## Key Architecture Rules
### Database
- Always use `mes_dashboard.core.database.read_sql_df()` for queries
- Never create direct connections in services
- Reset `db._ENGINE = None` in test setUp
### Caching
- Use `mes_dashboard.core.cache` for all caching operations
- Use `mes_dashboard.services.filter_cache` for workcenter/family lookups
- Always convert WORKCENTERNAME → WORKCENTER_GROUP for display
### Frontend
- Toast notifications: Use `Toast.warning()`, `Toast.error()`, `Toast.success()` (NOT MESToast)
- API calls: Use `MesApi.get()` with proper timeout
- Array operations: Remember `.reverse()` modifies in-place
### Data Tables
- DW_MES_RESOURCE: Use `PJ_ASSETSSTATUS` (not ASSETSTATUS), `LOCATIONNAME` (not LOCATION)
- DW_MES_RESOURCESTATUS_SHIFT: HISTORYID maps to RESOURCEID
- DW_PJ_LOT_V: Source for WORKCENTER_GROUP mapping
### SQL
- Use `/*+ MATERIALIZE */` hint for Oracle CTEs used multiple times
- Date range: `TXNDATE >= start AND TXNDATE < end + 1`
- Apply EQUIPMENT_TYPE_FILTER, location exclusions, asset status exclusions
## Testing
- Unit tests: `tests/test_*_service.py`
- Integration tests: `tests/test_*_routes.py`
- E2E tests: `tests/e2e/test_*_e2e.py`
- For parallel queries (ThreadPoolExecutor), mock with function-based side_effect, not list

View File

@@ -12,21 +12,129 @@
```python ```python
from mes_dashboard.core.database import read_sql_df, get_engine from mes_dashboard.core.database import read_sql_df, get_engine
# 讀取資料 # 讀取資料 (推薦方式)
df = read_sql_df(sql) df = read_sql_df(sql, params)
# 取得 engine若需要直接操作 # 取得 engine若需要直接操作
engine = get_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 中自行建立連線 - **禁止**在各 service 中自行建立連線
- **禁止**直接使用 `oracledb.connect()`
- 連線池由 `database.py` 統一管理,避免連線洩漏 - 連線池由 `database.py` 統一管理,避免連線洩漏
- 測試環境需在 setUp 中重置:`db._ENGINE = None` - 測試環境需在 setUp 中重置:`db._ENGINE = None`
--- ---
## 2. 快取機制 ## 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 ### 全局快取 API
使用 `mes_dashboard.core.cache` 模組: 使用 `mes_dashboard.core.cache` 模組:
@@ -49,13 +157,35 @@ if result is None:
### 快取 TTL 常數 ### 快取 TTL 常數
定義於 `mes_dashboard.config.constants` 定義於 `mes_dashboard.config.constants`
- `CACHE_TTL_FILTER_OPTIONS`: 篩選選項(較長)
- `CACHE_TTL_TREND`: 趨勢資料(中等) ```python
- `CACHE_TTL_REALTIME`: 即時資料(較短) 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` | 篩選選項快取 |
--- ---
## 3. Filter Cache篩選選項快取 ## 4. Filter Cache篩選選項快取
### 位置 ### 位置
`mes_dashboard.services.filter_cache` `mes_dashboard.services.filter_cache`
@@ -86,7 +216,306 @@ WORKCENTERNAME (資料庫) → WORKCENTER_GROUP (顯示)
--- ---
## 4. 前端全局組件 ## 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 通知 ### Toast 通知
定義於 `static/js/toast.js`,透過 `_base.html` 載入: 定義於 `static/js/toast.js`,透過 `_base.html` 載入:
@@ -96,59 +525,90 @@ WORKCENTERNAME (資料庫) → WORKCENTER_GROUP (顯示)
Toast.info('訊息'); Toast.info('訊息');
Toast.success('成功'); Toast.success('成功');
Toast.warning('警告'); Toast.warning('警告');
Toast.error('錯誤'); Toast.error('錯誤', { retry: () => loadData() });
Toast.loading('載入中...');
const id = Toast.loading('載入中...');
Toast.update(id, { message: '完成!' });
Toast.dismiss(id);
// 錯誤用法(不存在) // 錯誤用法(不存在)
MESToast.warning('...'); // ❌ 錯誤 MESToast.warning('...'); // ❌ 錯誤
``` ```
### 自動消失時間
- info: 3000ms
- success: 2000ms
- warning: 5000ms
- error: 永久(需手動關閉)
- loading: 永久
### MesApiHTTP 請求) ### MesApiHTTP 請求)
定義於 `static/js/api.js`,提供統一的 API 呼叫介面 定義於 `static/js/mes-api.js`
```javascript ```javascript
const result = await MesApi.get('/api/endpoint', { timeout: 30000 }); // GET 請求
if (result.success) { const data = await MesApi.get('/api/wip/summary', {
// 處理資料 params: { page: 1 },
} else { timeout: 60000,
Toast.error(result.error); 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 重試
--- ---
## 5. 資料表預篩選規則 ## 13. 資料表預篩選規則
### 設備類型篩選 ### 設備類型篩選
定義於 `mes_dashboard.config.constants.EQUIPMENT_TYPE_FILTER` 定義於 `mes_dashboard.config.constants.EQUIPMENT_TYPE_FILTER`
```sql ```sql
-- 只查詢特定設備類型 ((OBJECTCATEGORY = 'ASSEMBLY' AND OBJECTTYPE = 'ASSEMBLY')
r.EQUIPMENTTYPE IN ('主要設備', '輔助設備') OR (OBJECTCATEGORY = 'WAFERSORT' AND OBJECTTYPE = 'WAFERSORT'))
``` ```
### 排除條件 ### 排除條件
```python ```python
# 排除的地點 # 排除的地點
EXCLUDED_LOCATIONS = ['TEST', 'LAB', ...] EXCLUDED_LOCATIONS = [
'ATEC', 'F區', 'F區焊接站', '報廢', '實驗室',
'山東', '成型站_F區', '焊接F區', '無錫', '熒茂'
]
# 排除的資產狀態 # 排除的資產狀態
EXCLUDED_ASSET_STATUSES = ['報廢', '停用', ...] EXCLUDED_ASSET_STATUSES = ['Disapproved']
``` ```
### SQL 範例 ### CommonFilters 使用
```python 位置: `mes_dashboard.sql.filters`
# 建立篩選條件
location_filter = _build_location_filter('r')
# → AND (r.LOCATIONNAME IS NULL OR r.LOCATIONNAME NOT IN ('TEST', 'LAB'))
asset_status_filter = _build_asset_status_filter('r') ```python
# → AND r.PJ_ASSETSSTATUS NOT IN ('報廢', '停用') 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)
``` ```
--- ---
## 6. 資料庫欄位對應 ## 14. 資料庫欄位對應
### DW_MES_RESOURCE ### DW_MES_RESOURCE
| 常見錯誤 | 正確欄位名 | | 常見錯誤 | 正確欄位名 |
@@ -176,7 +636,7 @@ asset_status_filter = _build_asset_status_filter('r')
--- ---
## 7. E10 狀態定義 ## 15. E10 狀態定義
| 狀態 | 說明 | 計入 OU% | | 狀態 | 說明 | 計入 OU% |
|-----|------|---------| |-----|------|---------|
@@ -192,9 +652,103 @@ asset_status_filter = _build_asset_status_filter('r')
OU% = PRD / (PRD + SBY + UDT + SDT + EGT) × 100 OU% = PRD / (PRD + SBY + UDT + SDT + EGT) × 100
``` ```
### 狀態顯示名稱
```python
STATUS_DISPLAY_NAMES = {
'PRD': '生產中',
'SBY': '待機',
'UDT': '非計畫停機',
'SDT': '計畫停機',
'EGT': '工程時間',
'NST': '未排單',
}
```
--- ---
## 8. 平行查詢 ## 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 ### ThreadPoolExecutor
對於多個獨立查詢,使用平行執行提升效能: 對於多個獨立查詢,使用平行執行提升效能:
@@ -207,7 +761,6 @@ with ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(read_sql_df, kpi_sql): 'kpi', executor.submit(read_sql_df, kpi_sql): 'kpi',
executor.submit(read_sql_df, trend_sql): 'trend', executor.submit(read_sql_df, trend_sql): 'trend',
executor.submit(read_sql_df, heatmap_sql): 'heatmap', executor.submit(read_sql_df, heatmap_sql): 'heatmap',
executor.submit(read_sql_df, comparison_sql): 'comparison',
} }
for future in as_completed(futures): for future in as_completed(futures):
query_name = futures[future] query_name = futures[future]
@@ -220,7 +773,7 @@ with ThreadPoolExecutor(max_workers=4) as executor:
--- ---
## 9. Oracle SQL 優化 ## 18. Oracle SQL 優化
### CTE MATERIALIZE Hint ### CTE MATERIALIZE Hint
防止 Oracle 優化器將 CTE inline 多次執行: 防止 Oracle 優化器將 CTE inline 多次執行:
@@ -238,13 +791,17 @@ SELECT ...
### 日期範圍查詢 ### 日期範圍查詢
```sql ```sql
-- 包含 end_date 當天 -- 包含 end_date 當天
WHERE TXNDATE >= TO_DATE('{start_date}', 'YYYY-MM-DD') WHERE TXNDATE >= TO_DATE(:start_date, 'YYYY-MM-DD')
AND TXNDATE < TO_DATE('{end_date}', 'YYYY-MM-DD') + 1 AND TXNDATE < TO_DATE(:end_date, 'YYYY-MM-DD') + 1
``` ```
### 慢查詢警告
- 閾值: 1 秒 (警告)5 秒 (`SLOW_QUERY_THRESHOLD`)
- 自動記錄到日誌
--- ---
## 10. 前端資料限制 ## 19. 前端資料限制
### 明細資料上限 ### 明細資料上限
為避免瀏覽器記憶體問題,明細查詢有筆數限制: 為避免瀏覽器記憶體問題,明細查詢有筆數限制:
@@ -266,7 +823,7 @@ if (result.truncated) {
--- ---
## 11. JavaScript 注意事項 ## 20. JavaScript 注意事項
### Array.reverse() 原地修改 ### Array.reverse() 原地修改
```javascript ```javascript
@@ -282,15 +839,18 @@ const reversed = [...arr].reverse();
--- ---
## 12. 測試規範 ## 21. 測試規範
### 測試檔案結構 ### 測試檔案結構
``` ```
tests/ tests/
├── test_*_service.py # 單元測試service layer ├── conftest.py # pytest fixtures
├── test_*_routes.py # 整合測試(API endpoints ├── test_*_service.py # 單元測試(service layer
── e2e/ ── test_*_routes.py # 整合測試API endpoints
└── test_*_e2e.py # 端對端測試(完整流程) ├── e2e/
│ └── test_*_e2e.py # 端對端測試(完整流程)
└── stress/
└── test_*.py # 壓力測試
``` ```
### 測試前重置 ### 測試前重置
@@ -307,4 +867,70 @@ pytest tests/test_resource_history_service.py -v
# 全部相關測試 # 全部相關測試
pytest tests/test_resource_history_*.py tests/e2e/test_resource_history_e2e.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` |