Files
egg c8966477b9 feat: Initial commit - Task Reporter incident response system
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>
2025-12-01 17:42:52 +08:00

556 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 提升搜尋效能