-- ===================================================================== -- 知識庫系統 V5 — DB Schema(L1, PRD §5 / §27.5 / §34 / §49 對齊) -- SQLite WAL, foreign_keys=ON, busy_timeout=30000 -- 對 PRD 模糊處的補完,皆於該表上方註解 "PRD-FILL" 標註。 -- ===================================================================== -- ---- 系統 / migration ---- CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL ); -- ===================================================================== -- 核心 7 張(PRD §5.1) -- ===================================================================== -- ---- users(PRD §5.1)---- CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin','senior_manager','manager','user','guest')), permissions TEXT NOT NULL DEFAULT '{"categories":["all"]}', department TEXT NOT NULL DEFAULT '', expires_at TEXT, -- §50.2.2 guest 帳號自動失效時間 guest_allowed_depts TEXT NOT NULL DEFAULT '[]', -- §50.2.2 guest 可看部門 JSON created_at TEXT NOT NULL, last_login TEXT, is_active INTEGER NOT NULL DEFAULT 1, is_deleted INTEGER NOT NULL DEFAULT 0, deleted_at TEXT, deleted_by INTEGER REFERENCES users(id), force_password_change INTEGER NOT NULL DEFAULT 0, jwt_version INTEGER NOT NULL DEFAULT 1, totp_secret TEXT, totp_enabled INTEGER NOT NULL DEFAULT 0, totp_grace_until TEXT, failed_login_count INTEGER NOT NULL DEFAULT 0, locked_until TEXT ); -- PRD-FILL: documents 表 PRD §5.1 僅列擴充欄位,核心欄位依 §6 frontmatter + §11 visibility + §15 embedding 推導 CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, category TEXT NOT NULL, tags TEXT NOT NULL DEFAULT '[]', -- JSON array summary TEXT, body TEXT NOT NULL, -- Markdown vault_path TEXT UNIQUE, -- vault/ 對應 md 路徑 author_id INTEGER NOT NULL REFERENCES users(id), owner_department TEXT NOT NULL DEFAULT '', visibility TEXT NOT NULL DEFAULT 'department' CHECK(visibility IN ('public','department','selected_departments','restricted','confidential')), allowed_departments TEXT NOT NULL DEFAULT '[]', -- JSON array of dept names access_list TEXT NOT NULL DEFAULT '[]', -- JSON array of user_ids related TEXT NOT NULL DEFAULT '[]', -- §41 JSON array of related doc ids(防護C 跨部門互掛) up_moc_id INTEGER REFERENCES documents(id), -- §41 所屬統一 MOC 文件 id(防護D) keywords TEXT NOT NULL DEFAULT '[]', -- §8.5 JSON array of ai_keywords last_restored_from INTEGER, -- §7.5 最近一次還原的來源版次(任何 edit 後清空) template_id INTEGER REFERENCES document_templates(id), referrer_count INTEGER NOT NULL DEFAULT 0, view_count INTEGER NOT NULL DEFAULT 0, favorite_count INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived')), embedding BLOB, -- 384-dim float32 created_at TEXT NOT NULL, updated_at TEXT NOT NULL, is_deleted INTEGER NOT NULL DEFAULT 0, deleted_at TEXT, deleted_by INTEGER REFERENCES users(id) ); -- PRD-FILL: pending_documents 核心欄位(status CHECK 依 PRD §5.1 完整列舉) CREATE TABLE IF NOT EXISTS pending_documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, submitter_id INTEGER NOT NULL REFERENCES users(id), submitter_department TEXT NOT NULL DEFAULT '', original_content TEXT NOT NULL, ai_result TEXT, -- AI 整理後完整 markdown ai_meta_json TEXT, -- AI 提取的 frontmatter(標題/分類/標籤) target_document_id INTEGER REFERENCES documents(id), merge_target_title TEXT, status TEXT NOT NULL DEFAULT 'processing' CHECK(status IN ('processing','ai_ready','pending','approved','rejected','draft','failed')), reject_reason TEXT, reviewer_id INTEGER REFERENCES users(id), reviewed_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); -- PRD-FILL: document_history 版本追蹤 CREATE TABLE IF NOT EXISTS document_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, version INTEGER NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL, summary TEXT, tags TEXT NOT NULL DEFAULT '[]', edited_by INTEGER NOT NULL REFERENCES users(id), edited_at TEXT NOT NULL, change_note TEXT, UNIQUE(document_id, version) ); -- PRD-FILL: detection_rules(PRD §5.1 補欄位 + §49 layer1 規則) CREATE TABLE IF NOT EXISTS detection_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_name TEXT NOT NULL UNIQUE, pattern TEXT NOT NULL, -- regex layer INTEGER NOT NULL DEFAULT 1, -- 1=regex, 2=variant, 3=topic, 4=sample action TEXT NOT NULL DEFAULT 'block' CHECK(action IN ('block','warn','mask','log')), is_active INTEGER NOT NULL DEFAULT 1, is_deleted INTEGER NOT NULL DEFAULT 0, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL, updated_at TEXT, updated_by INTEGER REFERENCES users(id) ); -- PRD-FILL: audit_log(PRD §5.1 + §10.7 永久保留) CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), action TEXT NOT NULL, resource_type TEXT, resource_id TEXT, detail TEXT, -- JSON ip_address TEXT, user_agent TEXT, result TEXT CHECK(result IN ('success','fail','blocked')) DEFAULT 'success', timestamp TEXT NOT NULL ); -- PRD-FILL: notifications(PRD §5.1 30 天 TTL) CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, type TEXT NOT NULL, -- pending_review / approved / rejected / ... title TEXT NOT NULL, body TEXT, link TEXT, -- 點擊跳轉路徑 is_read INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, read_at TEXT ); -- ===================================================================== -- 衍生 5 張(PRD §5.2 / §27.3 / §30.2 / §31.1 / §32.2) -- ===================================================================== CREATE TABLE IF NOT EXISTS detection_rules_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_id INTEGER REFERENCES detection_rules(id), action TEXT NOT NULL CHECK(action IN ('create','update','delete','restore')), old_pattern TEXT, new_pattern TEXT, old_rule_name TEXT, new_rule_name TEXT, changed_by INTEGER REFERENCES users(id), changed_at TEXT NOT NULL, created_at TEXT NOT NULL ); -- PRD §27.3 CREATE TABLE IF NOT EXISTS ai_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), department TEXT, provider TEXT, model TEXT, action TEXT, input_tokens INTEGER, output_tokens INTEGER, cost_usd_x1000 INTEGER, cache_hit INTEGER NOT NULL DEFAULT 0, timestamp TEXT NOT NULL, pending_id INTEGER REFERENCES pending_documents(id) ); -- PRD §30.2 CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_agent TEXT, ip_address TEXT, created_at TEXT NOT NULL, last_used_at TEXT NOT NULL, expires_at TEXT NOT NULL ); -- PRD §31.1 CREATE TABLE IF NOT EXISTS feedback ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), category TEXT NOT NULL CHECK(category IN ('bug','feature','improvement','question','document_error')), title TEXT NOT NULL, description TEXT NOT NULL, page_url TEXT, doc_id INTEGER REFERENCES documents(id), -- §50.3.2 報告錯誤關聯文件 user_agent TEXT, status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','reviewing','resolved','wontfix')), admin_reply TEXT, created_at TEXT NOT NULL, updated_at TEXT ); -- PRD §32.2 CREATE TABLE IF NOT EXISTS user_preferences ( user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, notify_pending INTEGER NOT NULL DEFAULT 1, notify_account_apply INTEGER NOT NULL DEFAULT 1, notify_visibility INTEGER NOT NULL DEFAULT 1, notify_abnormal INTEGER NOT NULL DEFAULT 1, notify_announcement INTEGER NOT NULL DEFAULT 1, default_view TEXT NOT NULL DEFAULT 'grid' CHECK(default_view IN ('grid','list')), font_size TEXT NOT NULL DEFAULT 'medium' CHECK(font_size IN ('small','medium','large')), page_size INTEGER NOT NULL DEFAULT 50, seen_whatsnew_version TEXT, onboarding_completed INTEGER NOT NULL DEFAULT 0, onboarding_checklist_progress TEXT NOT NULL DEFAULT '{}' -- §50.3.1 {doc_id: viewed_at} ); -- ===================================================================== -- 其他(PRD §27.5 / §34 / §47 / §48 / §49 / 標籤 / 待辦 / 分享) -- ===================================================================== -- PRD §27.5.1 CREATE TABLE IF NOT EXISTS departments ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, is_active INTEGER NOT NULL DEFAULT 1, sort_order INTEGER NOT NULL DEFAULT 0, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL, deactivated_at TEXT, deactivated_by INTEGER REFERENCES users(id), onboarding_doc_ids TEXT NOT NULL DEFAULT '[]' -- §50.3.1 部門新人必讀文件 id JSON ); -- PRD §34.1 CREATE TABLE IF NOT EXISTS ai_cache ( content_hash TEXT PRIMARY KEY, ai_result_json TEXT NOT NULL, provider TEXT NOT NULL, model TEXT NOT NULL, cached_at TEXT NOT NULL, hit_count INTEGER NOT NULL DEFAULT 0, last_hit_at TEXT ); -- PRD-FILL: bookmarks(PRD §47 收藏) CREATE TABLE IF NOT EXISTS bookmarks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, folder TEXT NOT NULL DEFAULT '預設', -- 收藏分類 sort_order INTEGER NOT NULL DEFAULT 0, note TEXT, created_at TEXT NOT NULL, UNIQUE(user_id, document_id) ); -- PRD-FILL: bookmark_folders(PRD §12.25 收藏資料夾;支援空資料夾 + 改名/刪除) CREATE TABLE IF NOT EXISTS bookmark_folders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, icon TEXT, color TEXT, sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, UNIQUE(user_id, name) ); -- PRD §48 -- PRD §48.4 文檔模板系統:三件套(structure_md 骨架 / ai_prompt 指令 / example_content 範例) CREATE TABLE IF NOT EXISTS document_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, scope TEXT NOT NULL CHECK(scope IN ('global','department')), department_id INTEGER REFERENCES departments(id), -- scope='department' 時必填 structure_md TEXT NOT NULL DEFAULT '', -- 區塊骨架,AI 必須產出對應段落 ai_prompt TEXT NOT NULL DEFAULT '', -- 給 AI 的具體指令 example_content TEXT, -- 完整範例(選填) is_active INTEGER NOT NULL DEFAULT 1, is_default INTEGER NOT NULL DEFAULT 0, -- 該範圍的預設模板 created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL, updated_at TEXT, updated_by INTEGER REFERENCES users(id) ); -- PRD §41 防護D:跨部門同主題追蹤(admin 跨部門主題管理頁 §41.5) CREATE TABLE IF NOT EXISTS cross_dept_topics ( id INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL, -- 主題名(取自文件標題) doc_ids TEXT NOT NULL DEFAULT '[]', -- JSON 涉及文件 ids departments TEXT NOT NULL DEFAULT '[]', -- JSON 涉及部門 status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','moc_created','ignored')), moc_id INTEGER REFERENCES documents(id), -- 建立的統一 MOC 文件 ignored_until TEXT, -- ignored:30 天內不再通知 created_at TEXT NOT NULL, updated_at TEXT ); -- PRD §49.6 CREATE TABLE IF NOT EXISTS sensitive_topics ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, is_builtin INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, threshold REAL NOT NULL DEFAULT 0.65, centroid BLOB, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL, updated_at TEXT ); CREATE TABLE IF NOT EXISTS sensitive_topic_samples ( id INTEGER PRIMARY KEY AUTOINCREMENT, topic_id INTEGER NOT NULL REFERENCES sensitive_topics(id) ON DELETE CASCADE, sample_text TEXT NOT NULL, embedding BLOB NOT NULL, added_by INTEGER REFERENCES users(id), added_at TEXT NOT NULL ); -- PRD §49.7 CREATE TABLE IF NOT EXISTS confidential_samples ( id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL, description TEXT, embedding BLOB NOT NULL, text_length INTEGER, text_hash TEXT, added_by INTEGER REFERENCES users(id), added_at TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1 ); -- PRD §49.9 CREATE TABLE IF NOT EXISTS false_positive_appeals ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), text_snippet TEXT NOT NULL, layer INTEGER NOT NULL, matched_rule TEXT, similarity REAL, user_reason TEXT, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected')), reviewed_by INTEGER REFERENCES users(id), reviewed_at TEXT, admin_note TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS whitelist_samples ( id INTEGER PRIMARY KEY AUTOINCREMENT, embedding BLOB NOT NULL, label TEXT, added_by INTEGER REFERENCES users(id), added_at TEXT NOT NULL ); -- PRD-FILL: tags + document_tags(PRD §12.21 標籤頁所需;§6 frontmatter tags 為 JSON,這裡是正規化版本) CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, color TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS document_tags ( document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY(document_id, tag_id) ); -- PRD-FILL: tasks(PRD §12.24 個人待辦) CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, title TEXT NOT NULL, body TEXT, status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','in_progress','done','archived')), due_at TEXT, sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT ); -- PRD-FILL: share_links(PRD §11 分享連結) CREATE TABLE IF NOT EXISTS share_links ( id INTEGER PRIMARY KEY AUTOINCREMENT, document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, created_by INTEGER NOT NULL REFERENCES users(id), password_hash TEXT, require_login INTEGER NOT NULL DEFAULT 0, expires_at TEXT, revoked_at TEXT, created_at TEXT NOT NULL, last_used_at TEXT ); -- ===================================================================== -- 索引(PRD §5.3) -- ===================================================================== CREATE INDEX IF NOT EXISTS idx_documents_category ON documents(category); CREATE INDEX IF NOT EXISTS idx_documents_visibility ON documents(visibility, is_deleted); CREATE INDEX IF NOT EXISTS idx_pending_status ON pending_documents(status); CREATE INDEX IF NOT EXISTS idx_history_document ON document_history(document_id); CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id); CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp); CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, is_read); CREATE INDEX IF NOT EXISTS idx_users_department ON users(department, is_deleted); CREATE INDEX IF NOT EXISTS idx_ai_usage_user ON ai_usage(user_id, timestamp); CREATE INDEX IF NOT EXISTS idx_ai_usage_dept ON ai_usage(department, timestamp); CREATE INDEX IF NOT EXISTS idx_departments_active ON departments(is_active, sort_order); CREATE INDEX IF NOT EXISTS idx_ai_cache_cached_at ON ai_cache(cached_at); CREATE INDEX IF NOT EXISTS idx_conf_samples_active ON confidential_samples(is_active); CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); CREATE INDEX IF NOT EXISTS idx_bookmarks_user ON bookmarks(user_id, sort_order); CREATE INDEX IF NOT EXISTS idx_tasks_user ON tasks(user_id, status, sort_order); CREATE INDEX IF NOT EXISTS idx_share_links_doc ON share_links(document_id); CREATE INDEX IF NOT EXISTS idx_templates_scope ON document_templates(scope, department_id, is_active); CREATE INDEX IF NOT EXISTS idx_templates_default ON document_templates(is_default, is_active); -- ===================================================================== -- FTS5 全文索引(PRD §5.3) -- 採 contentless 對應 documents 主表,由觸發器同步 -- ===================================================================== CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(title, summary, body, content='documents', content_rowid='id'); CREATE TRIGGER IF NOT EXISTS documents_ai AFTER INSERT ON documents BEGIN INSERT INTO documents_fts(rowid, title, summary, body) VALUES (new.id, new.title, new.summary, new.body); END; CREATE TRIGGER IF NOT EXISTS documents_ad AFTER DELETE ON documents BEGIN INSERT INTO documents_fts(documents_fts, rowid, title, summary, body) VALUES ('delete', old.id, old.title, old.summary, old.body); END; CREATE TRIGGER IF NOT EXISTS documents_au AFTER UPDATE ON documents BEGIN INSERT INTO documents_fts(documents_fts, rowid, title, summary, body) VALUES ('delete', old.id, old.title, old.summary, old.body); INSERT INTO documents_fts(rowid, title, summary, body) VALUES (new.id, new.title, new.summary, new.body); END; -- ===================================================================== -- L3+ trusted_devices(記住此裝置以跳過 2FA),對應 HTML「記住我 14 天」 -- PRD §10.1 / §10.2 衍生:經過 2FA 驗證的裝置在 TTL 內登入可跳過 2FA。 -- 撤銷方式:使用者於 /account/sessions 移除、改密、jwt_version+1、過期 cron。 -- 只存 token 的 SHA-256,避免 DB 外洩。 -- ===================================================================== -- ===================================================================== -- L3+ users profile 擴充(migration v3):display_name / extension / -- secondary_departments(兼任部門 JSON)。PRD §5 未列,依使用者需求 + §7.3 gap。 -- 註:欄位若已存在,db.py 的 migration 會 try/except 略過(SQLite 無 IF NOT EXISTS for ADD COLUMN)。 -- ===================================================================== CREATE TABLE IF NOT EXISTS trusted_devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL, label TEXT, user_agent TEXT, ip_address TEXT, created_at TEXT NOT NULL, last_used_at TEXT NOT NULL, expires_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_trusted_devices_user ON trusted_devices(user_id, expires_at); CREATE UNIQUE INDEX IF NOT EXISTS idx_trusted_devices_token ON trusted_devices(token_hash); -- ===================================================================== -- L10:AI Provider 設定 / 模型對照 / 切換旗標 / 匯率快取(PRD §6.x / §27.3 / §33.4) -- key-value JSON 設定表(PRD §33.4 system_config.exchange_rates 等)。 -- ===================================================================== CREATE TABLE IF NOT EXISTS system_config ( key TEXT PRIMARY KEY, value TEXT NOT NULL, -- JSON updated_at TEXT NOT NULL, updated_by INTEGER REFERENCES users(id) ); -- §50.2.1 多部門兼任(users_departments 多對多,D234)。 -- 註:PRD 範例用 department_id,但本系統全鏈路為「部門名稱制」(owner_department / allowed_departments -- 皆存名稱),為一致性與降低重構風險,此處改存部門名稱 department(§7.1 取較一致一方)。 CREATE TABLE IF NOT EXISTS users_departments ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, department TEXT NOT NULL, is_primary INTEGER NOT NULL DEFAULT 0, assigned_at TEXT NOT NULL, assigned_by INTEGER REFERENCES users(id), PRIMARY KEY (user_id, department) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_user_primary ON users_departments(user_id) WHERE is_primary=1; CREATE INDEX IF NOT EXISTS idx_ai_usage_user ON ai_usage(user_id, timestamp); CREATE INDEX IF NOT EXISTS idx_ai_usage_dept ON ai_usage(department, timestamp); CREATE INDEX IF NOT EXISTS idx_ai_cache_cached_at ON ai_cache(cached_at);