Complete implementation of the production line incident response system (生產線異常即時反應系統) including: Backend (FastAPI): - User authentication with AD integration and session management - Chat room management (create, list, update, members, roles) - Real-time messaging via WebSocket (typing indicators, reactions) - File storage with MinIO (upload, download, image preview) Frontend (React + Vite): - Authentication flow with token management - Room list with filtering, search, and pagination - Real-time chat interface with WebSocket - File upload with drag-and-drop and image preview - Member management and room settings - Breadcrumb navigation - 53 unit tests (Vitest) Specifications: - authentication: AD auth, sessions, JWT tokens - chat-room: rooms, members, templates - realtime-messaging: WebSocket, messages, reactions - file-storage: MinIO integration, file management - frontend-core: React SPA structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
18 KiB
18 KiB
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 | 執行轉移的使用者 ⭐ |
索引策略
-- 主鍵索引
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;
設計考量
- UUID 主鍵: 使用 UUID 而非自增 ID,避免 ID 可預測性問題
- 軟刪除: 使用
archived_at進行軟刪除,保留歷史記錄 - 冗餘欄位:
member_count為冗餘欄位,提升查詢效能 - 稽核欄位:
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 | 移除時間(軟刪除標記) |
約束條件
-- 外鍵約束
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;
索引策略
-- 複合索引:用於快速查詢房間的特定成員
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;
設計考量
-
軟刪除: 使用
removed_at而非實際刪除記錄- 優點:保留成員歷史、支援稽核、可恢復誤刪
- 缺點:需要在所有查詢中加入
WHERE removed_at IS NULL
-
角色設計: 簡單的 ENUM 而非獨立的角色表
- 適合固定的少量角色
- 若未來需要動態角色,應改為關聯表設計
-
級聯刪除: 當房間刪除時,自動刪除所有成員記錄
- 配合軟刪除機制,實際不會觸發(房間只會被標記為 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 欄位範例
// 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
}
]
}
索引策略
-- 唯一索引:範本名稱必須唯一
CREATE UNIQUE INDEX ix_room_templates_name
ON room_templates(name);
-- 單欄索引:用於按類型查詢範本
CREATE INDEX ix_room_templates_incident_type
ON room_templates(incident_type);
設計考量
-
JSON 欄位: 使用 JSON 儲存結構化資料
- 優點:彈性高、易於擴展
- 缺點:無法建立索引、查詢效能較差
- 適用場景:讀取頻繁、寫入較少、資料結構可能變動
-
預設範本: 系統啟動時自動初始化三個預設範本
- equipment_failure(設備故障)
- material_shortage(物料短缺)
- quality_issue(品質問題)
關聯關係
1. incident_rooms ↔ room_members (1:N)
-- 一個房間可以有多個成員
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)
-- 一個使用者可以參與多個房間
-- 透過 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. 列出使用者的所有活躍房間
-- 優化前(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. 取得房間詳情與所有成員
-- 優化:使用單一查詢避免 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. 搜尋房間(全文搜尋)
-- 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. 業務規則約束
# 在應用層實施的約束
# 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. 資料庫級約束
-- 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 筆成員記錄
-- 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 冗餘欄位:
-- 不使用冗餘(每次都計算)
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. 初始化腳本
# 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. 資料備份策略
# 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 需要應用層維護一致性
⚠️ 無分散式支援 - 目前設計未考慮分散式部署
未來優化建議
- 分區表 - 當資料量達到百萬級時,考慮按時間分區
- 讀寫分離 - 使用主從複製提升讀取效能
- 快取層 - Redis 快取熱門房間資訊
- 全文搜尋引擎 - Elasticsearch 提升搜尋效能