# Database Schema Documentation ## 概述 Chat Room 模組使用三個主要資料表來管理事件室、成員關係與範本。本文檔說明資料庫架構、關聯關係、索引策略與設計考量。 --- ## ER Diagram (實體關聯圖) ``` ┌─────────────────────────────────────────────────────────────┐ │ incident_rooms │ ├─────────────────────────────────────────────────────────────┤ │ PK │ room_id (VARCHAR(36), UUID) │ │ │ title (VARCHAR(255), NOT NULL) │ │ │ incident_type (ENUM, NOT NULL) │ │ │ severity (ENUM, NOT NULL) │ │ │ status (ENUM, DEFAULT 'ACTIVE') │ │ │ location (VARCHAR(255)) │ │ │ description (TEXT) │ │ │ resolution_notes (TEXT, NULLABLE) │ │ │ created_by (VARCHAR(255), NOT NULL) │ │ │ created_at (TIMESTAMP, DEFAULT NOW) │ │ │ resolved_at (TIMESTAMP, NULLABLE) │ │ │ archived_at (TIMESTAMP, NULLABLE) │ │ │ last_activity_at (TIMESTAMP, DEFAULT NOW) │ │ │ last_updated_at (TIMESTAMP, DEFAULT NOW) │ │ │ member_count (INTEGER, DEFAULT 0) │ │ │ ownership_transferred_at (TIMESTAMP, NULLABLE) ⭐ │ │ │ ownership_transferred_by (VARCHAR(255), NULLABLE) ⭐ │ └─────────────────────────────────────────────────────────────┘ │ │ 1:N ↓ ┌─────────────────────────────────────────────────────────────┐ │ room_members │ ├─────────────────────────────────────────────────────────────┤ │ PK │ id (INTEGER, AUTO_INCREMENT) │ │ FK │ room_id (VARCHAR(36)) ──→ incident_rooms.room_id │ │ │ user_id (VARCHAR(255), NOT NULL) │ │ │ role (ENUM, NOT NULL) │ │ │ added_by (VARCHAR(255), NOT NULL) │ │ │ added_at (TIMESTAMP, DEFAULT NOW) │ │ │ removed_at (TIMESTAMP, NULLABLE) ← 軟刪除 │ │ │ │ │ │ UNIQUE (room_id, user_id) WHERE removed_at IS NULL │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ room_templates │ ├─────────────────────────────────────────────────────────────┤ │ PK │ template_id (INTEGER, AUTO_INCREMENT) │ │ │ name (VARCHAR(100), UNIQUE, NOT NULL) │ │ │ description (TEXT) │ │ │ incident_type (ENUM, NOT NULL) │ │ │ default_severity (ENUM, NOT NULL) │ │ │ default_members (JSON) │ │ │ metadata_fields (JSON, NULLABLE) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 資料表詳細說明 ### 1. incident_rooms (事件室) **用途**: 儲存生產線異常事件室的核心資訊 #### 欄位說明 | 欄位名稱 | 資料型別 | 約束 | 說明 | |---------|---------|------|-----| | `room_id` | VARCHAR(36) | PK, NOT NULL | UUID 格式的房間唯一識別碼 | | `title` | VARCHAR(255) | NOT NULL | 事件標題 | | `incident_type` | ENUM | NOT NULL | 事件類型:EQUIPMENT_FAILURE, MATERIAL_SHORTAGE, QUALITY_ISSUE, OTHER | | `severity` | ENUM | NOT NULL | 嚴重程度:LOW, MEDIUM, HIGH, CRITICAL | | `status` | ENUM | DEFAULT 'ACTIVE' | 房間狀態:ACTIVE, RESOLVED, ARCHIVED | | `location` | VARCHAR(255) | | 事件發生地點 | | `description` | TEXT | | 事件詳細描述 | | `resolution_notes` | TEXT | NULLABLE | 解決方案說明(狀態為 RESOLVED 時填寫) | | `created_by` | VARCHAR(255) | NOT NULL | 建立者的使用者 ID(信箱) | | `created_at` | TIMESTAMP | DEFAULT NOW | 建立時間 | | `resolved_at` | TIMESTAMP | NULLABLE | 標記為已解決的時間 | | `archived_at` | TIMESTAMP | NULLABLE | 封存時間(軟刪除標記) | | `last_activity_at` | TIMESTAMP | DEFAULT NOW | 最後活動時間(用於排序) | | `last_updated_at` | TIMESTAMP | DEFAULT NOW | 最後更新時間 | | `member_count` | INTEGER | DEFAULT 0 | 當前活躍成員數量 | | `ownership_transferred_at` | TIMESTAMP | NULLABLE | **所有權轉移時間** ⭐ | | `ownership_transferred_by` | VARCHAR(255) | NULLABLE | **執行轉移的使用者** ⭐ | #### 索引策略 ```sql -- 主鍵索引 CREATE UNIQUE INDEX pk_incident_rooms ON incident_rooms(room_id); -- 複合索引:用於按狀態與時間查詢 CREATE INDEX ix_incident_rooms_status_created ON incident_rooms(status, created_at DESC); -- 單欄索引:用於查詢特定使用者建立的房間 CREATE INDEX ix_incident_rooms_created_by ON incident_rooms(created_by); -- 用於查詢活躍房間(軟刪除) CREATE INDEX ix_incident_rooms_archived ON incident_rooms(archived_at) WHERE archived_at IS NULL; ``` #### 設計考量 1. **UUID 主鍵**: 使用 UUID 而非自增 ID,避免 ID 可預測性問題 2. **軟刪除**: 使用 `archived_at` 進行軟刪除,保留歷史記錄 3. **冗餘欄位**: `member_count` 為冗餘欄位,提升查詢效能 4. **稽核欄位**: `ownership_transferred_at` 和 `ownership_transferred_by` 用於追蹤所有權變更 --- ### 2. room_members (房間成員) **用途**: 管理房間成員關係與權限 #### 欄位說明 | 欄位名稱 | 資料型別 | 約束 | 說明 | |---------|---------|------|-----| | `id` | INTEGER | PK, AUTO_INCREMENT | 自增主鍵 | | `room_id` | VARCHAR(36) | FK, NOT NULL | 關聯到 incident_rooms.room_id | | `user_id` | VARCHAR(255) | NOT NULL | 使用者 ID(信箱) | | `role` | ENUM | NOT NULL | 角色:OWNER, EDITOR, VIEWER | | `added_by` | VARCHAR(255) | NOT NULL | 新增此成員的使用者 | | `added_at` | TIMESTAMP | DEFAULT NOW | 加入時間 | | `removed_at` | TIMESTAMP | NULLABLE | 移除時間(軟刪除標記) | #### 約束條件 ```sql -- 外鍵約束 ALTER TABLE room_members ADD CONSTRAINT fk_room_members_room_id FOREIGN KEY (room_id) REFERENCES incident_rooms(room_id) ON DELETE CASCADE; -- 唯一性約束:同一房間中,每個使用者只能有一個活躍成員記錄 -- SQLite CREATE UNIQUE INDEX ix_room_members_unique_active ON room_members(room_id, user_id) WHERE removed_at IS NULL; -- PostgreSQL CREATE UNIQUE INDEX ix_room_members_unique_active ON room_members(room_id, user_id) WHERE removed_at IS NULL; ``` #### 索引策略 ```sql -- 複合索引:用於快速查詢房間的特定成員 CREATE INDEX ix_room_members_room_user ON room_members(room_id, user_id); -- 單欄索引:用於查詢使用者參與的所有房間 CREATE INDEX ix_room_members_user ON room_members(user_id); -- 用於查詢活躍成員 CREATE INDEX ix_room_members_active ON room_members(removed_at) WHERE removed_at IS NULL; ``` #### 設計考量 1. **軟刪除**: 使用 `removed_at` 而非實際刪除記錄 - 優點:保留成員歷史、支援稽核、可恢復誤刪 - 缺點:需要在所有查詢中加入 `WHERE removed_at IS NULL` 2. **角色設計**: 簡單的 ENUM 而非獨立的角色表 - 適合固定的少量角色 - 若未來需要動態角色,應改為關聯表設計 3. **級聯刪除**: 當房間刪除時,自動刪除所有成員記錄 - 配合軟刪除機制,實際不會觸發(房間只會被標記為 archived) --- ### 3. room_templates (房間範本) **用途**: 儲存預定義的房間範本,用於快速建立標準化事件室 #### 欄位說明 | 欄位名稱 | 資料型別 | 約束 | 說明 | |---------|---------|------|-----| | `template_id` | INTEGER | PK, AUTO_INCREMENT | 範本唯一識別碼 | | `name` | VARCHAR(100) | UNIQUE, NOT NULL | 範本名稱(如 "equipment_failure") | | `description` | TEXT | | 範本說明 | | `incident_type` | ENUM | NOT NULL | 預設事件類型 | | `default_severity` | ENUM | NOT NULL | 預設嚴重程度 | | `default_members` | JSON | | 預設成員列表(JSON 陣列) | | `metadata_fields` | JSON | NULLABLE | 擴展用元資料欄位 | #### JSON 欄位範例 ```json // default_members 範例 [ { "user_id": "maintenance_team@panjit.com.tw", "role": "editor" }, { "user_id": "engineering@panjit.com.tw", "role": "viewer" } ] // metadata_fields 範例(未來擴展) { "required_fields": ["equipment_id", "line_number"], "custom_fields": [ { "name": "equipment_id", "type": "string", "required": true } ] } ``` #### 索引策略 ```sql -- 唯一索引:範本名稱必須唯一 CREATE UNIQUE INDEX ix_room_templates_name ON room_templates(name); -- 單欄索引:用於按類型查詢範本 CREATE INDEX ix_room_templates_incident_type ON room_templates(incident_type); ``` #### 設計考量 1. **JSON 欄位**: 使用 JSON 儲存結構化資料 - 優點:彈性高、易於擴展 - 缺點:無法建立索引、查詢效能較差 - 適用場景:讀取頻繁、寫入較少、資料結構可能變動 2. **預設範本**: 系統啟動時自動初始化三個預設範本 - equipment_failure(設備故障) - material_shortage(物料短缺) - quality_issue(品質問題) --- ## 關聯關係 ### 1. incident_rooms ↔ room_members (1:N) ```sql -- 一個房間可以有多個成員 SELECT r.*, m.user_id, m.role FROM incident_rooms r LEFT JOIN room_members m ON r.room_id = m.room_id WHERE m.removed_at IS NULL AND r.archived_at IS NULL; ``` ### 2. 使用者 ↔ room_members ↔ incident_rooms (M:N) ```sql -- 一個使用者可以參與多個房間 -- 透過 room_members 作為中介表實現多對多關係 SELECT r.* FROM incident_rooms r INNER JOIN room_members m ON r.room_id = m.room_id WHERE m.user_id = 'user@panjit.com.tw' AND m.removed_at IS NULL AND r.archived_at IS NULL ORDER BY r.last_activity_at DESC; ``` --- ## 常見查詢模式與優化 ### 1. 列出使用者的所有活躍房間 ```sql -- 優化前(N+1 問題) SELECT * FROM incident_rooms WHERE room_id IN ( SELECT room_id FROM room_members WHERE user_id = ? AND removed_at IS NULL ); -- 優化後(使用 JOIN) SELECT r.*, m.role as user_role FROM incident_rooms r INNER JOIN room_members m ON r.room_id = m.room_id WHERE m.user_id = ? AND m.removed_at IS NULL AND r.archived_at IS NULL ORDER BY r.last_activity_at DESC LIMIT ? OFFSET ?; -- 索引使用: -- - ix_room_members_user (user_id) -- - ix_incident_rooms_archived (archived_at) ``` ### 2. 取得房間詳情與所有成員 ```sql -- 優化:使用單一查詢避免 N+1 SELECT r.*, json_group_array( json_object( 'user_id', m.user_id, 'role', m.role, 'added_at', m.added_at ) ) as members FROM incident_rooms r LEFT JOIN room_members m ON r.room_id = m.room_id AND m.removed_at IS NULL WHERE r.room_id = ? GROUP BY r.room_id; -- 索引使用: -- - pk_incident_rooms (room_id) -- - ix_room_members_room_user (room_id, user_id) ``` ### 3. 搜尋房間(全文搜尋) ```sql -- SQLite FTS5(全文搜尋) CREATE VIRTUAL TABLE incident_rooms_fts USING fts5( room_id UNINDEXED, title, description, location ); -- 搜尋查詢 SELECT r.* FROM incident_rooms r INNER JOIN incident_rooms_fts fts ON r.room_id = fts.room_id WHERE fts MATCH '設備 OR 故障' AND r.archived_at IS NULL; -- PostgreSQL(使用 pg_trgm) CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX ix_incident_rooms_search ON incident_rooms USING gin ( to_tsvector('chinese', title || ' ' || description || ' ' || location) ); SELECT * FROM incident_rooms WHERE to_tsvector('chinese', title || ' ' || description || ' ' || location) @@ to_tsquery('chinese', '設備 & 故障') AND archived_at IS NULL; ``` --- ## 資料完整性與約束 ### 1. 業務規則約束 ```python # 在應用層實施的約束 # 1. 房間至少要有一個 OWNER def validate_room_has_owner(db, room_id): owner_count = db.query(RoomMember).filter( RoomMember.room_id == room_id, RoomMember.role == MemberRole.OWNER, RoomMember.removed_at.is_(None) ).count() if owner_count == 0: raise ValueError("Room must have at least one OWNER") # 2. 狀態轉換規則 VALID_TRANSITIONS = { RoomStatus.ACTIVE: [RoomStatus.RESOLVED], RoomStatus.RESOLVED: [RoomStatus.ARCHIVED], RoomStatus.ARCHIVED: [] } def validate_status_transition(current_status, new_status): if new_status not in VALID_TRANSITIONS.get(current_status, []): raise ValueError(f"Invalid transition: {current_status} -> {new_status}") ``` ### 2. 資料庫級約束 ```sql -- CHECK 約束(僅 PostgreSQL 支援) ALTER TABLE incident_rooms ADD CONSTRAINT chk_severity CHECK ( severity IN ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL') ); ALTER TABLE room_members ADD CONSTRAINT chk_role CHECK ( role IN ('OWNER', 'EDITOR', 'VIEWER') ); -- 確保 resolved_at 晚於 created_at ALTER TABLE incident_rooms ADD CONSTRAINT chk_resolved_after_created CHECK ( resolved_at IS NULL OR resolved_at >= created_at ); ``` --- ## 效能考量 ### 1. 索引策略總結 | 資料表 | 索引 | 類型 | 用途 | |-------|------|------|------| | incident_rooms | room_id | PRIMARY KEY | 主鍵 | | incident_rooms | (status, created_at) | COMPOSITE | 狀態篩選與排序 | | incident_rooms | created_by | SINGLE | 建立者查詢 | | room_members | (room_id, user_id) | COMPOSITE | 房間成員查詢 | | room_members | user_id | SINGLE | 使用者房間列表 | | room_members | (room_id, user_id) WHERE removed_at IS NULL | UNIQUE PARTIAL | 唯一性約束 | | room_templates | name | UNIQUE | 範本名稱查詢 | ### 2. 查詢效能估算 假設資料量: - 10,000 個房間 - 平均每個房間 5 個成員 = 50,000 筆成員記錄 ```sql -- 1. 查詢使用者的房間(有索引) -- 預期:<10ms EXPLAIN QUERY PLAN SELECT r.* FROM incident_rooms r INNER JOIN room_members m ON r.room_id = m.room_id WHERE m.user_id = 'user@example.com' AND m.removed_at IS NULL; -- 使用索引:ix_room_members_user -- 2. 查詢房間成員(有索引) -- 預期:<5ms EXPLAIN QUERY PLAN SELECT * FROM room_members WHERE room_id = 'xxx' AND removed_at IS NULL; -- 使用索引:ix_room_members_room_user ``` ### 3. 冗餘欄位權衡 **member_count 冗餘欄位**: ```sql -- 不使用冗餘(每次都計算) SELECT COUNT(*) FROM room_members WHERE room_id = ? AND removed_at IS NULL; -- 成本:每次查詢都需要掃描 room_members -- 使用冗餘(直接讀取) SELECT member_count FROM incident_rooms WHERE room_id = ?; -- 成本:寫入時需要同步更新,但讀取極快 -- 權衡:讀取頻率 >> 寫入頻率 → 使用冗餘欄位 ``` --- ## 資料遷移策略 ### 1. 初始化腳本 ```python # alembic/versions/001_create_chat_room_tables.py def upgrade(): # 1. 建立 incident_rooms 表 op.create_table( 'incident_rooms', sa.Column('room_id', sa.String(36), primary_key=True), sa.Column('title', sa.String(255), nullable=False), # ... 其他欄位 ) # 2. 建立 room_members 表 op.create_table( 'room_members', sa.Column('id', sa.Integer, primary_key=True, autoincrement=True), # ... 其他欄位 ) # 3. 建立索引 op.create_index('ix_incident_rooms_status_created', 'incident_rooms', ['status', 'created_at']) # ... 其他索引 def downgrade(): op.drop_table('room_members') op.drop_table('incident_rooms') op.drop_table('room_templates') ``` ### 2. 資料備份策略 ```bash # SQLite 備份 sqlite3 task_reporter.db ".backup task_reporter_backup.db" # PostgreSQL 備份 pg_dump -U postgres -d task_reporter > backup.sql # 恢復 psql -U postgres -d task_reporter < backup.sql ``` --- ## 總結 ### 架構優勢 ✅ **正規化設計** - 避免資料重複,保持一致性 ✅ **軟刪除機制** - 保留歷史記錄,支援稽核 ✅ **索引優化** - 覆蓋常見查詢模式 ✅ **彈性擴展** - JSON 欄位支援未來需求變更 ✅ **稽核追蹤** - 記錄所有關鍵操作(包含所有權轉移) ### 架構限制 ⚠️ **軟刪除開銷** - 所有查詢需加入 `removed_at IS NULL` ⚠️ **JSON 查詢限制** - default_members 無法有效索引 ⚠️ **冗餘欄位同步** - member_count 需要應用層維護一致性 ⚠️ **無分散式支援** - 目前設計未考慮分散式部署 ### 未來優化建議 1. **分區表** - 當資料量達到百萬級時,考慮按時間分區 2. **讀寫分離** - 使用主從複製提升讀取效能 3. **快取層** - Redis 快取熱門房間資訊 4. **全文搜尋引擎** - Elasticsearch 提升搜尋效能