Files
Task_Reporter/openspec/changes/archive/2025-11-17-add-chat-room-management/database-schema-documentation.md
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

18 KiB
Raw Blame History

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;

設計考量

  1. UUID 主鍵: 使用 UUID 而非自增 ID避免 ID 可預測性問題
  2. 軟刪除: 使用 archived_at 進行軟刪除,保留歷史記錄
  3. 冗餘欄位: member_count 為冗餘欄位,提升查詢效能
  4. 稽核欄位: ownership_transferred_atownership_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;

設計考量

  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 欄位範例

// 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);

設計考量

  1. JSON 欄位: 使用 JSON 儲存結構化資料

    • 優點:彈性高、易於擴展
    • 缺點:無法建立索引、查詢效能較差
    • 適用場景:讀取頻繁、寫入較少、資料結構可能變動
  2. 預設範本: 系統啟動時自動初始化三個預設範本

    • 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 需要應用層維護一致性 ⚠️ 無分散式支援 - 目前設計未考慮分散式部署

未來優化建議

  1. 分區表 - 當資料量達到百萬級時,考慮按時間分區
  2. 讀寫分離 - 使用主從複製提升讀取效能
  3. 快取層 - Redis 快取熱門房間資訊
  4. 全文搜尋引擎 - Elasticsearch 提升搜尋效能