# v1-SQL Schema ## Member ```sql -- 會員表 CREATE TABLE members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, full_name VARCHAR(100), --* 看有沒有支援 ENUM、確定系統角色是否可重複,另外統一 org 或是 com member_type VARCHAR(20) NOT NULL DEFAULT 'general', -- 'general', 'company_user', 'company_admin' is_verified BOOLEAN DEFAULT FALSE, is_active BOOLEAN DEFAULT TRUE, avatar_url VARCHAR(255), bio TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_login_at TIMESTAMP WITH TIME ZONE, last_active_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 會員驗證表 CREATE TABLE member_verifications ( --* 可能不需要 id, 可以直接用 member_id 當 PK id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL REFERENCES members(id) ON DELETE CASCADE, verification_type VARCHAR(20) NOT NULL, -- 'email', 'password_reset' verification_code VARCHAR(100) NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, is_used BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 會員會話表 --* 要跟宇衡確定 JWT refresh token 的 session 要存那哪些欄位 CREATE TABLE member_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL REFERENCES members(id) ON DELETE CASCADE, session_token VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, ip_address INET, user_agent TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 會員通知設定表 (對應個人設定頁面) CREATE TABLE member_notification_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL REFERENCES members(id) ON DELETE CASCADE, email_notifications BOOLEAN DEFAULT TRUE, -- Email Notifications activity_updates BOOLEAN DEFAULT TRUE, -- Activity Updates marketing_emails BOOLEAN DEFAULT FALSE, -- Marketing Emails created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); --* 可能需要多一張表、存 org-member 的對應提升效能 --* 例如列出某個 org 底下的所有 member 時,若 member DB 中有紀錄 org-member,可以直接 SQL 一次撈 --* 但如果只有 org DB 中有 member-org,需要一筆一筆跟 member svc 拿 member data -- 索引 CREATE INDEX idx_members_email ON members(email); CREATE INDEX idx_members_username ON members(username); CREATE INDEX idx_members_type ON members(member_type); CREATE INDEX idx_members_active ON members(is_active); CREATE INDEX idx_members_last_active ON members(last_active_at); CREATE INDEX idx_member_verifications_code ON member_verifications(verification_code); CREATE INDEX idx_member_sessions_token ON member_sessions(session_token); CREATE INDEX idx_member_sessions_member_id ON member_sessions(member_id); ``` ## Organization ```sql -- 企業組織表 (對應Company Profile頁面) CREATE TABLE organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, -- Company Name industry VARCHAR(50), -- Industry website VARCHAR(255), -- Company Website description TEXT, -- Company Description admin_member_id UUID NOT NULL, -- 企業後台管理者 --* 看能不能 ENUM、或是用 true/false subscription_plan VARCHAR(20) DEFAULT 'free', -- 'free', 'premium' is_active BOOLEAN DEFAULT TRUE, logo_url VARCHAR(255), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 組織成員表 (對應Team Members和User Management頁面) --* 若 member 也存 org-member,可以斟酌這邊完全保留、或是只留設定的部分 CREATE TABLE organization_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, member_id UUID NOT NULL, -- 參考 Member Service 的 member.id role VARCHAR(20) NOT NULL DEFAULT 'member', -- 'admin', 'member' -- 權限設定 (對應Permissions欄位) can_post BOOLEAN DEFAULT FALSE, -- Can post can_moderate BOOLEAN DEFAULT FALSE, -- Can moderate can_invite BOOLEAN DEFAULT FALSE, -- Can invite status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'active', 'inactive' (對應Status欄位) joined_at TIMESTAMP WITH TIME ZONE, invited_by UUID, -- 邀請者ID created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(organization_id, member_id) ); -- 成員邀請表 --* 同樣的、可以討論 member DB 是否需要一份 replica CREATE TABLE member_invitations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, inviter_member_id UUID NOT NULL, -- 邀請者 invitee_email VARCHAR(100) NOT NULL, invitation_token VARCHAR(255) UNIQUE NOT NULL, status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'accepted', 'rejected', 'expired' expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 組織設定表 CREATE TABLE organization_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, -- 安全設定 (對應Account Settings頁面) two_factor_authentication BOOLEAN DEFAULT FALSE, api_access BOOLEAN DEFAULT FALSE, email_notifications BOOLEAN DEFAULT TRUE, settings JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 索引 CREATE INDEX idx_organizations_admin ON organizations(admin_member_id); CREATE INDEX idx_org_members_org_id ON organization_members(organization_id); CREATE INDEX idx_org_members_member_id ON organization_members(member_id); CREATE INDEX idx_org_members_status ON organization_members(status); CREATE INDEX idx_invitations_token ON member_invitations(invitation_token); CREATE INDEX idx_invitations_email ON member_invitations(invitee_email); ``` ## Admin ```sql -- 系統管理員表 CREATE TABLE system_admins ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role VARCHAR(20) DEFAULT 'admin', -- 'super_admin', 'admin', 'moderator' permissions JSONB, is_active BOOLEAN DEFAULT TRUE, last_login_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 內容審核表 (支援Q&A的Approved/Pending Review狀態) CREATE TABLE content_reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content_type VARCHAR(20) NOT NULL, -- 'experience_post', 'qa_discussion' content_id UUID NOT NULL, content_service VARCHAR(50) NOT NULL, -- 來源服務名稱 reviewer_id UUID REFERENCES system_admins(id), -- AI檢測結果 ai_score DECIMAL(3,2), -- AI評分 0-1 ai_analysis JSONB, -- AI分析結果 is_sustainability_related BOOLEAN DEFAULT FALSE, -- 審核狀態 manual_review_required BOOLEAN DEFAULT FALSE, status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'approved', 'rejected', 'flagged' review_notes TEXT, reviewed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 系統設定表 CREATE TABLE system_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), setting_key VARCHAR(100) UNIQUE NOT NULL, setting_value JSONB NOT NULL, description TEXT, is_public BOOLEAN DEFAULT FALSE, updated_by UUID REFERENCES system_admins(id), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 系統日誌表 --! 改用 ES,直接 log 出來 EFK 收 CREATE TABLE system_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), service_name VARCHAR(50) NOT NULL, log_level VARCHAR(10) NOT NULL, -- 'info', 'warning', 'error', 'critical' message TEXT NOT NULL, metadata JSONB, member_id UUID, -- 關聯的會員ID organization_id UUID, -- 關聯的組織ID ip_address INET, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 企業審核表 CREATE TABLE organization_reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL, reviewer_id UUID REFERENCES system_admins(id), review_type VARCHAR(20) NOT NULL, -- 'registration', 'subscription_upgrade' status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'approved', 'rejected' review_notes TEXT, reviewed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 索引 CREATE INDEX idx_content_reviews_content ON content_reviews(content_type, content_id); CREATE INDEX idx_content_reviews_status ON content_reviews(status); CREATE INDEX idx_content_reviews_reviewer ON content_reviews(reviewer_id); CREATE INDEX idx_system_logs_service ON system_logs(service_name); CREATE INDEX idx_system_logs_level ON system_logs(log_level); CREATE INDEX idx_system_logs_created ON system_logs(created_at); CREATE INDEX idx_org_reviews_org_id ON organization_reviews(organization_id); CREATE INDEX idx_org_reviews_status ON organization_reviews(status); ``` ## Content ```sql -- 標籤表 (對應hashtag功能) CREATE TABLE tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(50) UNIQUE NOT NULL, usage_count INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); --* 有沒有機會 ES/OpenSearch 或 Mongo 全文搜尋更快 還能 RAG (x -- 經驗分享文章表 (對應Experience Sharing頁面) CREATE TABLE experience_posts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), author_member_id UUID NOT NULL, -- 參考 Member Service organization_id UUID, -- 參考 Organization Service (發文者所屬組織) title VARCHAR(200), content TEXT NOT NULL, media_urls JSONB, -- 存儲圖片和影片URL status VARCHAR(20) DEFAULT 'published', -- 'draft', 'published', 'archived' -- 互動統計 view_count INTEGER DEFAULT 0, like_count INTEGER DEFAULT 0, comment_count INTEGER DEFAULT 0, share_count INTEGER DEFAULT 0, -- AI檢測 is_sdg_related BOOLEAN DEFAULT FALSE, ai_analysis JSONB, published_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 文章標籤關聯表 (支援hashtag功能) CREATE TABLE post_tag_relations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), post_id UUID NOT NULL REFERENCES experience_posts(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(post_id, tag_id) ); --* 這感覺也能用 no-SQL -- 文章留言表 (對應Comments功能) CREATE TABLE post_comments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), post_id UUID NOT NULL REFERENCES experience_posts(id) ON DELETE CASCADE, author_member_id UUID NOT NULL, -- 參考 Member Service content TEXT NOT NULL CHECK (LENGTH(content) <= 100), -- 100字限制 like_count INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Q&A討論表 (對應Q&A Zone頁面) CREATE TABLE qa_discussions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), author_member_id UUID NOT NULL, -- 參考 Member Service organization_id UUID, -- 參考 Organization Service title VARCHAR(200) NOT NULL, content TEXT NOT NULL, -- 標籤 (如 #sustainability, #manufacturing) tags JSONB, -- 狀態管理 status VARCHAR(20) DEFAULT 'published', -- 'pending', 'published', 'approved', 'rejected' approval_status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'approved' (對應Approved標籤) -- 統計 view_count INTEGER DEFAULT 0, answer_count INTEGER DEFAULT 0, like_count INTEGER DEFAULT 0, is_resolved BOOLEAN DEFAULT FALSE, is_sustainability_related BOOLEAN DEFAULT FALSE, -- AI檢測結果 created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Q&A回覆表 (對應Responses功能) CREATE TABLE qa_answers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), discussion_id UUID NOT NULL REFERENCES qa_discussions(id) ON DELETE CASCADE, author_member_id UUID NOT NULL, -- 參考 Member Service content TEXT NOT NULL CHECK (LENGTH(content) <= 100), -- 100字限制 like_count INTEGER DEFAULT 0, is_best_answer BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 會員互動表 (點讚、分享等) CREATE TABLE member_interactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL, -- 參考 Member Service target_type VARCHAR(20) NOT NULL, -- 'post', 'comment', 'qa_discussion', 'qa_answer' target_id UUID NOT NULL, interaction_type VARCHAR(20) NOT NULL, -- 'like', 'share', 'bookmark' created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(member_id, target_type, target_id, interaction_type) ); -- 索引 CREATE INDEX idx_experience_posts_author ON experience_posts(author_member_id); CREATE INDEX idx_experience_posts_org ON experience_posts(organization_id); CREATE INDEX idx_experience_posts_status ON experience_posts(status); CREATE INDEX idx_experience_posts_published ON experience_posts(published_at); CREATE INDEX idx_post_comments_post_id ON post_comments(post_id); CREATE INDEX idx_post_comments_author ON post_comments(author_member_id); CREATE INDEX idx_qa_discussions_author ON qa_discussions(author_member_id); CREATE INDEX idx_qa_discussions_status ON qa_discussions(status); CREATE INDEX idx_qa_discussions_approval ON qa_discussions(approval_status); CREATE INDEX idx_qa_answers_discussion ON qa_answers(discussion_id); CREATE INDEX idx_qa_answers_author ON qa_answers(author_member_id); CREATE INDEX idx_member_interactions_member ON member_interactions(member_id); -- 全文搜索索引 (支援搜索功能) CREATE INDEX idx_experience_posts_search ON experience_posts USING gin(to_tsvector('english', COALESCE(title, '') || ' ' || content)); CREATE INDEX idx_qa_discussions_search ON qa_discussions USING gin(to_tsvector('english', title || ' ' || content)); ``` ## Exchange ```sql -- 會員點數表 -- * 可以考慮放 member (視後續 API 功能決定) CREATE TABLE member_points ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL, -- 參考 Member Service total_points INTEGER DEFAULT 0, available_points INTEGER DEFAULT 0, used_points INTEGER DEFAULT 0, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 會員點數歷史記錄 CREATE TABLE member_point_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL, -- 參考 Member Service point_change INTEGER NOT NULL, point_type VARCHAR(20) NOT NULL, -- 'earned', 'used', 'expired' source_type VARCHAR(50), -- 'event_participation', 'exchange', 'manual_adjustment' source_id UUID, -- 關聯的活動或兌換記錄ID description TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 兌換商品分類表 CREATE TABLE exchange_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(50) UNIQUE NOT NULL, description TEXT, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 兌換商品表 CREATE TABLE exchange_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID, -- 參考 Organization Service (提供商品的組織) category_id UUID REFERENCES exchange_categories(id), name VARCHAR(100) NOT NULL, description TEXT NOT NULL, points_required INTEGER NOT NULL, stock_quantity INTEGER DEFAULT 0, unlimited_stock BOOLEAN DEFAULT FALSE, image_urls JSONB, terms_and_conditions TEXT, status VARCHAR(20) DEFAULT 'active', -- 'active', 'inactive', 'out_of_stock' valid_until TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 兌換記錄表 CREATE TABLE exchange_orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL, -- 參考 Member Service item_id UUID NOT NULL REFERENCES exchange_items(id), organization_id UUID, -- 參考 Organization Service points_used INTEGER NOT NULL, quantity INTEGER DEFAULT 1, status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'confirmed', 'shipped', 'completed', 'cancelled' delivery_info JSONB, -- 配送資訊 tracking_number VARCHAR(100), notes TEXT, ordered_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 兌換記錄狀態歷史 CREATE TABLE exchange_order_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), order_id UUID NOT NULL REFERENCES exchange_orders(id) ON DELETE CASCADE, previous_status VARCHAR(20), new_status VARCHAR(20) NOT NULL, changed_by UUID, -- 操作者ID notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 庫存變動記錄 CREATE TABLE exchange_stock_movements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), item_id UUID NOT NULL REFERENCES exchange_items(id) ON DELETE CASCADE, movement_type VARCHAR(20) NOT NULL, -- 'add', 'remove', 'exchange', 'adjustment' quantity_change INTEGER NOT NULL, reference_id UUID, -- 關聯的訂單或其他記錄ID notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 索引 CREATE INDEX idx_member_points_member_id ON member_points(member_id); CREATE INDEX idx_member_point_history_member_id ON member_point_history(member_id); CREATE INDEX idx_member_point_history_type ON member_point_history(point_type); CREATE INDEX idx_exchange_items_org_id ON exchange_items(organization_id); CREATE INDEX idx_exchange_items_category ON exchange_items(category_id); CREATE INDEX idx_exchange_items_status ON exchange_items(status); CREATE INDEX idx_exchange_orders_member ON exchange_orders(member_id); CREATE INDEX idx_exchange_orders_item ON exchange_orders(item_id); CREATE INDEX idx_exchange_orders_status ON exchange_orders(status); CREATE INDEX idx_exchange_orders_org ON exchange_orders(organization_id); CREATE INDEX idx_exchange_order_history_order ON exchange_order_history(order_id); CREATE INDEX idx_stock_movements_item ON exchange_stock_movements(item_id); ``` ## Analytics ```sql -- 首頁統計數據表 (對應首頁儀表板數據) CREATE TABLE dashboard_stats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), date DATE NOT NULL, organization_id UUID, -- 可選,組織級別統計 -- 對應首頁顯示的數據 experience_shared_count INTEGER DEFAULT 0, -- Experience Shared: 245 questions_asked_count INTEGER DEFAULT 0, -- Questions Asked: 128 active_users_count INTEGER DEFAULT 0, -- Active Users: 1,024 -- 其他統計 total_members INTEGER DEFAULT 0, new_members_today INTEGER DEFAULT 0, total_posts INTEGER DEFAULT 0, total_comments INTEGER DEFAULT 0, total_likes INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(date, organization_id) ); -- 會員活動統計表 CREATE TABLE member_activity_stats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL, -- 參考 Member Service date DATE NOT NULL, -- 活動統計 posts_created INTEGER DEFAULT 0, comments_made INTEGER DEFAULT 0, qa_questions_asked INTEGER DEFAULT 0, qa_answers_given INTEGER DEFAULT 0, likes_given INTEGER DEFAULT 0, shares_made INTEGER DEFAULT 0, login_count INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(member_id, date) ); -- 內容統計表 (支援Featured Content) CREATE TABLE content_stats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), date DATE NOT NULL, organization_id UUID, -- 可選 -- 內容統計 experience_posts_created INTEGER DEFAULT 0, qa_discussions_created INTEGER DEFAULT 0, comments_created INTEGER DEFAULT 0, qa_answers_created INTEGER DEFAULT 0, -- 互動統計 total_views INTEGER DEFAULT 0, total_likes INTEGER DEFAULT 0, total_shares INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(date, organization_id) ); -- 搜索統計表 (支援搜索功能分析) CREATE TABLE search_stats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), search_query VARCHAR(255) NOT NULL, search_type VARCHAR(20) NOT NULL, -- 'post', 'qa', 'tag', 'hashtag' result_count INTEGER DEFAULT 0, member_id UUID, -- 參考 Member Service organization_id UUID, -- 參考 Organization Service created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 熱門內容表 (支援Community Highlights) CREATE TABLE trending_content ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content_type VARCHAR(20) NOT NULL, -- 'experience_post', 'qa_discussion' content_id UUID NOT NULL, organization_id UUID, -- 參考 Organization Service trend_score DECIMAL(10,4) DEFAULT 0, -- 熱門分數 trend_period VARCHAR(20) NOT NULL, -- 'daily', 'weekly', 'monthly' -- 計算熱門度的因子 view_count INTEGER DEFAULT 0, like_count INTEGER DEFAULT 0, comment_count INTEGER DEFAULT 0, share_count INTEGER DEFAULT 0, calculated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), valid_until TIMESTAMP WITH TIME ZONE NOT NULL, UNIQUE(content_type, content_id, trend_period) ); -- 索引 CREATE INDEX idx_dashboard_stats_date ON dashboard_stats(date); CREATE INDEX idx_dashboard_stats_org ON dashboard_stats(organization_id); CREATE INDEX idx_member_activity_member ON member_activity_stats(member_id); CREATE INDEX idx_member_activity_date ON member_activity_stats(date); CREATE INDEX idx_content_stats_date ON content_stats(date); CREATE INDEX idx_content_stats_org ON content_stats(organization_id); CREATE INDEX idx_search_stats_query ON search_stats(search_query); CREATE INDEX idx_search_stats_type ON search_stats(search_type); CREATE INDEX idx_search_stats_created ON search_stats(created_at); CREATE INDEX idx_trending_content_type ON trending_content(content_type); CREATE INDEX idx_trending_content_score ON trending_content(trend_score); CREATE INDEX idx_trending_content_period ON trending_content(trend_period); ``` # 問題統整 ## Member DB 問題 1. **`--* 看有沒有支援 ENUM、確定系統角色是否可重複,另外統一 org 或是 com`** - 位置:members 表的 member_type 欄位 - 問題:需要確認資料庫是否支援 ENUM 類型、系統角色重複性、命名統一性 2. **`--* 可能不需要 id, 可以直接用 member_id 當 PK`** - 位置:member_verifications 表 - 問題:考慮是否移除獨立的 id 欄位,直接使用 member_id 作為主鍵 3. **`--* 要跟宇衡確定 JWT refresh token 的 session 要存那哪些欄位`** - 位置:member_sessions 表 - 問題:需要與宇衡確認 JWT refresh token 機制所需的欄位 4. **`--* 可能需要多一張表、存 org-member 的對應提升效能`** - 位置:索引區塊前 - 問題:考慮是否需要額外的組織-會員對應表來提升查詢效能 5. **`--* 例如列出某個 org 底下的所有 member 時,若 member DB 中有紀錄 org-member,可以直接 SQL 一次撈`** - 位置:同上 - 問題:說明效能考量的具體場景 6. **`--* 但如果只有 org DB 中有 member-org,需要一筆一筆跟 member svc 拿 member data`** - 位置:同上 - 問題:說明現有架構可能的效能瓶頸 ## Organization DB 問題 7. **`--* 看能不能 ENUM、或是用 true/false`** - 位置:organizations 表的 subscription_plan 欄位 - 問題:考慮使用 ENUM 或布林值來替代 VARCHAR 8. **`--* 若 member 也存 org-member,可以斟酌這邊完全保留、或是只留設定的部分`** - 位置:organization_members 表 - 問題:如果 member DB 也存儲組織關聯,需要考慮資料重複性 9. **`--* 同樣的、可以討論 member DB 是否需要一份 replica`** - 位置:member_invitations 表 - 問題:考慮在 member DB 中是否需要邀請資料的副本 ## Content DB 問題 10. **`--* 有沒有機會 ES/OpenSearch 或 Mongo 全文搜尋更快 還能 RAG (x`** - 位置:tags 表註解 - 問題:考慮使用 Elasticsearch/OpenSearch 或 MongoDB 來提升搜尋效能和支援 RAG 11. **`--* 這感覺也能用 no-SQL`** - 位置:post_comments 表註解 - 問題:考慮使用 NoSQL 資料庫來存儲留言資料 ## Exchange DB 問題 12. **`--* 可以考慮放 member (視後續 API 功能決定)`** - 位置:member_points 表註解 - 問題:考慮是否將點數資料放在 member service 中 ## Admin DB 問題 13. **`--! 改用 ES,直接 log 出來 EFK 收`** - 位置:system_logs 表註解 - 問題:建議改用 Elasticsearch,通過 EFK (Elasticsearch, Fluentd, Kibana) 來收集日誌 # 改進 - ENUM 類型 + 複合索引 - 移除不必要的 UUID 欄位 - 本地存儲組織關聯 - 專用的 token 索引 | 服務 | 原始問題 | 修正項目 | 修正效果 | |------|----------|----------|----------| | **Member Service** | `--* 看有沒有支援 ENUM、確定系統角色是否可重複,另外統一 org 或是 com` | 使用 `member_type_enum` 替代 VARCHAR | 數據一致性提升、查詢效能提升 | | Member Service | `--* 可能不需要 id, 可以直接用 member_id 當 PK` | `member_verifications` 使用複合主鍵 `(member_id, verification_type)` | 節省空間、避免無意義ID | | Member Service | `--* 要跟宇衡確定 JWT refresh token 的 session 要存那哪些欄位` | 新增 `refresh_token`, `refresh_expires_at`, `token_type` 欄位 | 支援完整JWT流程 | | Member Service | `--* 可能需要多一張表、存 org-member 的對應提升效能` | 新增 `member_organizations` 表 | 避免跨服務查詢、提升效能 | | **Organization Service** | `--* 看能不能 ENUM、或是用 true/false` | 使用 `subscription_plan_enum` 或 `is_premium BOOLEAN` | 訂閱狀態管理標準化 | | Organization Service | `--* 若 member 也存 org-member,可以斟酌這邊完全保留、或是只留設定的部分` | 保留完整 `organization_members` 表,負責詳細權限管理 | 職責分離、避免數據重複 | | Organization Service | `--* 同樣的、可以討論 member DB 是否需要一份 replica` | 建議不在 Member DB 複製邀請資料 | 維持單一資料源 | | **Admin Service** | 字串比較效能問題 | 所有狀態欄位使用 ENUM 類型 | 狀態管理標準化、查詢效能提升 | | Admin Service | `--! 改用 ES,直接 log 出來 EFK 收` | 移除 `system_logs` 表 | 符合 ELK Stack 架構 | | **Content Service** | `--* 有沒有機會 ES/OpenSearch 或 Mongo 全文搜尋更快 還能 RAG (x` | 保持 PostgreSQL GIN 索引,建議搭配 ES | 搜尋效能提升、支援未來RAG | | Content Service | `--* 這感覺也能用 no-SQL` | 保持 PostgreSQL 結構 | 維持ACID特性、暫不遷移 | | Content Service | 標籤關聯表效能 | `post_tag_relations` 使用複合主鍵 `(post_id, tag_id)` | 提升效能、節省空間 | | **Exchange Service** | `-- * 可以考慮放 member (視後續 API 功能決定)` | `member_points` 直接用 `member_id` 當主鍵,保留在 Exchange Service | 避免跨服務查詢複雜度 | | Exchange Service | 狀態管理混亂 | 所有狀態欄位使用 ENUM 類型 | 狀態流程標準化 | | **Analytics Service** | 字串比較效能 | `search_type`, `content_type`, `trend_period` 使用 ENUM | 查詢效能提升 | --- # v2-SQL Schema > 接下來以國展想看的 demo 為主,我提議依序搞定 admin、member、organization ## Activity Service Database ```sql -- 動態表單系統擴展 -- 表單欄位類型枚舉 CREATE TYPE field_type_enum AS ENUM ( 'short_text', -- 短文字 (單行輸入) 'long_text', -- 長文字 (多行輸入) 'number', -- 數字 'email', -- 電子郵件 'phone', -- 電話 'date', -- 日期 'time', -- 時間 'datetime', -- 日期時間 'single_choice', -- 單選 (radio) 'multiple_choice', -- 多選 (checkbox) 'dropdown', -- 下拉選單 'file_upload', -- 檔案上傳 'rating', -- 評分 'yes_no', -- 是/否 'url', -- 網址 'address' -- 地址 ); -- 表單狀態枚舉 CREATE TYPE form_status_enum AS ENUM ( 'draft', -- 草稿 'active', -- 啟用中 'closed', -- 已關閉 'archived' -- 已封存 ); -- 表單模板表 (定義表單結構) CREATE TABLE form_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL, -- 所屬組織 creator_user_id UUID NOT NULL, -- 創建者 activity_id UUID REFERENCES activities(id) ON DELETE SET NULL, -- 可選:關聯到特定活動 title VARCHAR(255) NOT NULL, -- 表單標題 description TEXT, -- 表單說明 status form_status_enum DEFAULT 'draft', -- 表單狀態 -- 進階設定 is_public BOOLEAN DEFAULT false, -- 是否公開 (可匿名填寫) allow_multiple_submissions BOOLEAN DEFAULT false, -- 是否允許重複提交 require_login BOOLEAN DEFAULT true, -- 是否需要登入才能填寫 show_progress_bar BOOLEAN DEFAULT false, -- 是否顯示進度條 submit_button_text VARCHAR(50) DEFAULT '提交', -- 提交按鈕文字 success_message TEXT DEFAULT '感謝您的提交!', -- 提交成功訊息 -- 時間設定 open_time TIMESTAMP WITH TIME ZONE, -- 開放填寫時間 close_time TIMESTAMP WITH TIME ZONE, -- 關閉填寫時間 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 表單欄位表 (定義每個欄位的屬性) CREATE TABLE form_fields ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), form_template_id UUID NOT NULL REFERENCES form_templates(id) ON DELETE CASCADE, field_type field_type_enum NOT NULL, -- 欄位類型 label VARCHAR(255) NOT NULL, -- 欄位標籤/問題 description TEXT, -- 欄位說明/提示文字 placeholder VARCHAR(255), -- 佔位符文字 -- 驗證規則 is_required BOOLEAN DEFAULT false, -- 是否必填 min_length INTEGER, -- 最小長度 (文字欄位) max_length INTEGER, -- 最大長度 (文字欄位) min_value NUMERIC(15, 2), -- 最小值 (數字欄位) max_value NUMERIC(15, 2), -- 最大值 (數字欄位) validation_regex TEXT, -- 自訂驗證正則表達式 validation_message TEXT, -- 驗證失敗訊息 -- 排序與顯示 display_order INTEGER NOT NULL, -- 顯示順序 is_visible BOOLEAN DEFAULT true, -- 是否顯示 -- 條件邏輯 (進階功能) conditional_logic JSONB, -- 條件顯示邏輯 (JSON格式) created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 表單欄位選項表 (用於單選、多選、下拉選單等) CREATE TABLE form_field_options ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), form_field_id UUID NOT NULL REFERENCES form_fields(id) ON DELETE CASCADE, option_value TEXT NOT NULL, -- 選項值 option_label VARCHAR(255) NOT NULL, -- 選項顯示文字 display_order INTEGER NOT NULL, -- 顯示順序 is_default BOOLEAN DEFAULT false, -- 是否為預設選項 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 表單提交記錄表 (儲存每次提交) CREATE TABLE form_submissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), form_template_id UUID NOT NULL REFERENCES form_templates(id) ON DELETE CASCADE, submitter_user_id UUID, -- 提交者 (如果已登入) submitter_ip VARCHAR(45), -- 提交者 IP submitter_email VARCHAR(255), -- 提交者 Email (如果收集) is_completed BOOLEAN DEFAULT false, -- 是否完成提交 (支援草稿功能) submission_data JSONB NOT NULL, -- 提交的表單資料 (JSON格式,靈活儲存) submitted_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 表單提交回應表 (詳細儲存每個欄位的回應) CREATE TABLE form_field_responses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), form_submission_id UUID NOT NULL REFERENCES form_submissions(id) ON DELETE CASCADE, form_field_id UUID NOT NULL REFERENCES form_fields(id) ON DELETE CASCADE, response_value TEXT, -- 回應值 (文字形式) response_data JSONB, -- 回應資料 (JSON格式,支援複雜資料) created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 表單檔案上傳表 (如果欄位包含檔案上傳) CREATE TABLE form_file_uploads ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), form_field_response_id UUID NOT NULL REFERENCES form_field_responses(id) ON DELETE CASCADE, file_name VARCHAR(255) NOT NULL, -- 檔案名稱 file_size BIGINT NOT NULL, -- 檔案大小 (bytes) file_type VARCHAR(100), -- 檔案類型 (MIME type) file_url TEXT NOT NULL, -- 檔案儲存路徑/URL uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 表單統計表 (快取表單統計資料) CREATE TABLE form_statistics ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), form_template_id UUID NOT NULL REFERENCES form_templates(id) ON DELETE CASCADE, total_views INTEGER DEFAULT 0, -- 總瀏覽次數 total_submissions INTEGER DEFAULT 0, -- 總提交次數 completion_rate NUMERIC(5, 2), -- 完成率 average_completion_time INTEGER, -- 平均完成時間 (秒) last_calculated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 索引優化 CREATE INDEX idx_form_templates_org_id ON form_templates(organization_id); CREATE INDEX idx_form_templates_activity_id ON form_templates(activity_id); CREATE INDEX idx_form_templates_status ON form_templates(status); CREATE INDEX idx_form_fields_template_id ON form_fields(form_template_id); CREATE INDEX idx_form_fields_display_order ON form_fields(form_template_id, display_order); CREATE INDEX idx_form_field_options_field_id ON form_field_options(form_field_id); CREATE INDEX idx_form_submissions_template_id ON form_submissions(form_template_id); CREATE INDEX idx_form_submissions_user_id ON form_submissions(submitter_user_id); CREATE INDEX idx_form_submissions_submitted_at ON form_submissions(submitted_at); CREATE INDEX idx_form_field_responses_submission_id ON form_field_responses(form_submission_id); CREATE INDEX idx_form_file_uploads_response_id ON form_file_uploads(form_field_response_id); -- JSONB 索引 (加速 JSON 查詢) CREATE INDEX idx_form_submissions_data ON form_submissions USING gin(submission_data); CREATE INDEX idx_form_field_responses_data ON form_field_responses USING gin(response_data); -- 自動更新 updated_at 的觸發器函數 CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ language 'plpgsql'; -- 為需要的表創建觸發器 CREATE TRIGGER update_form_templates_updated_at BEFORE UPDATE ON form_templates FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_form_fields_updated_at BEFORE UPDATE ON form_fields FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_form_submissions_updated_at BEFORE UPDATE ON form_submissions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ## Admin Service Database ```sql -- Admin Service Database -- 平台管理員操作日誌 CREATE TABLE platform_admin_audit_log ( id BIGSERIAL PRIMARY KEY, admin_user_id UUID NOT NULL, -- 執行操作的平台管理員 User ID (來自 Member Service) action_type VARCHAR(100) NOT NULL, -- 例如: 'approve_organization', 'reject_organization', 'suspend_user', 'update_system_setting' target_entity_type VARCHAR(50), -- 例如: 'organization', 'user', 'article' target_entity_id VARCHAR(36), -- 被操作的實體 ID (應該是 UUID) details JSONB, -- 操作細節 ip_address INET, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 系統設定表 (可選,若有全域設定需求) CREATE TABLE system_settings ( setting_key VARCHAR(100) PRIMARY KEY, setting_value TEXT, description TEXT, updated_by_admin_user_id UUID, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- (可選) 審核佇列表 (如果需要一個集中的地方管理各類審核任務) -- 實際上審核狀態更多是分散在各自服務的實體中 (如 Organization 的 status, Article 的 status) -- 此表更像是一個給平台管理員看的「待辦事項」摘要 -- CREATE TABLE admin_review_queue ( -- id BIGSERIAL PRIMARY KEY, -- item_type VARCHAR(50) NOT NULL, -- 'organization_application', 'article_flagged', 'question_flagged' -- item_id UUID NOT NULL, -- 對應到具體服務中的實體 ID -- reason_for_review TEXT, -- assigned_to_admin_user_id UUID, -- status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'in_progress', 'resolved' -- created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -- ); -- 索引 CREATE INDEX idx_platform_admin_audit_log_admin_user_id ON platform_admin_audit_log(admin_user_id); CREATE INDEX idx_platform_admin_audit_log_target_entity ON platform_admin_audit_log(target_entity_type, target_entity_id); -- CREATE INDEX idx_admin_review_queue_status ON admin_review_queue(status); ``` ## Analytics Service Database ```sql -- Analytics Service Database -- 每日使用者活動快照 (範例) CREATE TABLE daily_user_activity_summary ( summary_date DATE PRIMARY KEY, active_users_count INT, -- 活躍使用者數量 new_registrations_count INT, -- ... 其他使用者相關指標 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 每日內容互動快照 (範例) CREATE TABLE daily_content_summary ( summary_date DATE PRIMARY KEY, articles_published_count INT, questions_asked_count INT, comments_made_count INT, answers_given_count INT, -- 其他指標 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 企業活動概覽 CREATE TABLE organization_activity_overview ( organization_id UUID, summary_date DATE, active_users_in_org_count INT, articles_posted_by_org_users_count INT, activities_created_count INT, PRIMARY KEY (organization_id, summary_date), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 儀表板用:最近的文章/問題 (這更像是一個快取或投影,資料源是 Content Service) CREATE TABLE recent_dashboard_items ( item_id UUID PRIMARY KEY, -- article_id 或 question_id item_type VARCHAR(20) NOT NULL, -- 'article', 'question' title VARCHAR(255), author_display_name VARCHAR(100), -- 冗餘顯示用 organization_display_name VARCHAR(200), -- 冗餘顯示用 created_or_published_at TIMESTAMP WITH TIME ZONE, -- 用於排序和快速提取 display_priority INT DEFAULT 0, data_payload JSONB, -- 原始項目摘要,方便前端直接使用 refreshed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 索引 (針對查詢優化) CREATE INDEX idx_daily_user_activity_summary_date ON daily_user_activity_summary(summary_date); CREATE INDEX idx_daily_content_summary_date ON daily_content_summary(summary_date); CREATE INDEX idx_organization_activity_overview_org_date ON organization_activity_overview(organization_id, summary_date); CREATE INDEX idx_recent_dashboard_items_type_date ON recent_dashboard_items(item_type, created_or_published_at DESC); ``` ## Content Service Database ```sql -- Content Service Database -- 內容審核狀態枚舉 CREATE TYPE content_status_enum AS ENUM ( 'draft', -- 草稿 (若有此功能) 'pending_review', -- 等待審核 (SDGs/ESG 相關性) 'published', -- 已發布 'rejected', -- 審核未通過 'archived' -- 已封存/下架 ); -- 文章表 (經驗分享文章) CREATE TABLE articles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(255) NOT NULL, content TEXT NOT NULL, -- 主要文字內容 -- 作者資訊 (從 Member Service 獲取) author_user_id UUID NOT NULL, -- 必須是 'organization_user' author_organization_id UUID, -- 作者所屬企業 ID (方便查詢) status content_status_enum NOT NULL DEFAULT 'pending_review', rejection_reason TEXT, -- 審核未通過原因 -- 圖片與影片連結 (使用 JSONB 儲存陣列,或另開表) -- 範例:[{ "type": "image", "url": "...", "caption": "..." }, { "type": "video", "url": "...", "provider": "youtube" }] media_attachments JSONB, view_count INT DEFAULT 0, like_count INT DEFAULT 0, -- 若有按讚功能 published_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- tags CREATE TABLE tags ( id SERIAL PRIMARY KEY, -- 使用 SERIAL 或 UUID 皆可 name VARCHAR(50) UNIQUE NOT NULL, description TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 文章與標籤關聯表 (多對多) CREATE TABLE article_tags ( article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, tag_id INT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (article_id, tag_id) ); -- 文章留言表 CREATE TABLE article_comments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, commenter_user_id UUID NOT NULL, -- 任何已登入使用者皆可留言 parent_comment_id UUID REFERENCES article_comments(id) ON DELETE CASCADE, -- 用於巢狀留言 content VARCHAR(100) NOT NULL, -- 文字上限 100 字 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 問題討論串表 (發問區) CREATE TABLE questions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(255) NOT NULL, -- 問題標題/簡述 details TEXT, -- 問題詳細描述 (純文字) asker_user_id UUID NOT NULL, -- 任何已登入使用者皆可提問 asker_organization_id UUID, -- 提問者所屬企業ID (若適用) status content_status_enum NOT NULL DEFAULT 'pending_review', -- 問題也需審核 SDGs/ESG 相關性 rejection_reason TEXT, view_count INT DEFAULT 0, answer_count INT DEFAULT 0, -- 回答數量 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_activity_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -- 最後回答或編輯時間 ); -- 問題回答表 CREATE TABLE Youtubes ( -- *** 原 Youtubes 表已更名 *** id UUID PRIMARY KEY DEFAULT gen_random_uuid(), question_id UUID NOT NULL REFERENCES questions(id) ON DELETE CASCADE, answerer_user_id UUID NOT NULL, -- 任何已登入使用者皆可回答 content TEXT NOT NULL, -- 純文字回答 is_accepted_answer BOOLEAN DEFAULT FALSE, -- 若有最佳答案功能 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 索引 CREATE INDEX idx_articles_author_user_id ON articles(author_user_id); CREATE INDEX idx_articles_status ON articles(status); CREATE INDEX idx_article_tags_tag_id ON article_tags(tag_id); CREATE INDEX idx_article_comments_article_id ON article_comments(article_id); CREATE INDEX idx_questions_asker_user_id ON questions(asker_user_id); CREATE INDEX idx_questions_status ON questions(status); CREATE INDEX idx_Youtubes_question_id ON Youtubes(question_id); -- *** 索引名稱與對應表已更新 *** -- 全文搜索相關的索引 (依賴具體資料庫,例如 PostgreSQL 的 GIN/GIST 索引) -- CREATE INDEX idx_articles_content_search ON articles USING gin(to_tsvector('chinese', title || ' ' || content)); -- CREATE INDEX idx_questions_content_search ON questions USING gin(to_tsvector('chinese', title || ' ' || details)); -- CREATE INDEX idx_tags_name_search ON tags USING gin(to_tsvector('chinese', name)); ``` ## Member Service Database ```sql -- Member Service Database -- 使用者身份枚舉 CREATE TYPE member_role_enum AS ENUM ( 'platform_admin', -- 平台管理者 'general_user', -- 一般使用者 'organization_admin', -- 企業後臺管理者 'organization_user' -- 企業使用者 ); -- email驗證枚舉 CREATE TYPE email_verification_enum AS ENUM ( 'email_verification', 'password_reset', ); -- 使用者帳號表 CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(50) UNIQUE NOT NULL, -- 使用者帳號名稱 email VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, full_name VARCHAR(100), -- 真實姓名或暱稱 avatar_url VARCHAR(255), -- 頭像圖片URL bio TEXT, -- 個人簡介 role member_role_enum NOT NULL DEFAULT 'general_user', -- 使用者身份 -- 關聯到 Organization Service 的 organization_id -- 對於 platform_admin 和 general_user,此欄位為 NULL -- 對於 organization_admin 和 organization_user,此欄位必要 organization_id UUID, -- 冗餘企業名稱,方便 Member Service 內部快速查詢,但主要來源是 Organization Service organization_name VARCHAR(200), is_email_verified BOOLEAN DEFAULT FALSE, -- Email 認證了沒 is_active BOOLEAN DEFAULT TRUE, -- 帳號是否啟用 (用於軟刪除或禁用) status_reason TEXT, -- 帳號狀態變更原因(要?) created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_login_at TIMESTAMP WITH TIME ZONE ); -- 使用者 Email 驗證碼/密碼重設 Token 錶 CREATE TABLE user_verifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, verification_type VARCHAR(20) NOT NULL, -- 'email_verification', 'password_reset' token VARCHAR(100) UNIQUE NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, is_used BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 使用者會話表 (用於管理登入狀態,可選,取決於認證方案) CREATE TABLE user_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, session_token VARCHAR(255) UNIQUE NOT NULL, refresh_token VARCHAR(255) UNIQUE, ip_address INET, user_agent TEXT, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 使用者通知設定表 CREATE TABLE user_notification_settings ( user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, email_notifications_enabled BOOLEAN DEFAULT TRUE, -- 不累嘎總開關:是否接收郵件通知 -- 可以再細分更多通知類型,例如: -- new_post_in_followed_topic BOOLEAN DEFAULT TRUE, -- new_reply_to_my_comment BOOLEAN DEFAULT TRUE, -- activity_updates BOOLEAN DEFAULT TRUE, -- 來自活動模組的更新 updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 索引 CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_username ON users(username); CREATE INDEX idx_users_role ON users(role); CREATE INDEX idx_users_organization_id ON users(organization_id) WHERE organization_id IS NOT NULL; CREATE INDEX idx_user_verifications_token ON user_verifications(token); CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id); ``` ## Organization Service Database ```sql -- Organization Service Database -- 企業帳號申請狀態枚舉 CREATE TYPE organization_status_enum AS ENUM ( 'pending_approval', -- 等待平台管理者審核 'approved', -- 已核准 'rejected', -- 已拒絕 'active', -- 活躍 (核准後) 'suspended' -- 已暫停 ); -- 系統版本枚舉 (免費版/付費版) CREATE TYPE system_version_enum AS ENUM ( 'free', -- 免費版 'paid' -- 付費版 ); -- 企業表 CREATE TABLE organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(200) UNIQUE NOT NULL, -- 企業名稱 -- 企業後台管理者 (對應到 Member Service 的 user_id) -- 此 user_id 的 role 應為 'organization_admin' admin_user_id UUID UNIQUE, status organization_status_enum NOT NULL DEFAULT 'pending_approval', status_reason TEXT, -- 狀態變更原因 -- 新增:企業基本資訊欄位 industry VARCHAR(100), -- 行業別 website VARCHAR(255), -- 企業網站 description TEXT, -- 企業描述 current_version system_version_enum NOT NULL DEFAULT 'free', subscription_start_date TIMESTAMP WITH TIME ZONE, subscription_end_date TIMESTAMP WITH TIME ZONE, -- 付費版的到期日 -- 其他企業相關資訊 (可擴充) -- address TEXT, -- contact_phone VARCHAR(30), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, approved_by UUID, -- 平台管理者 User ID (如果需要記錄是誰核准的) approved_at TIMESTAMP WITH TIME ZONE ); -- 企業成員表 (用於管理企業使用者與企業的關係) CREATE TABLE organization_members ( organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, -- 對應到 Member Service 的 user_id -- 此 user_id 的 role 應為 'organization_user' 或 'organization_admin' user_id UUID NOT NULL, member_status organization_status_enum NOT NULL DEFAULT 'pending_approval', -- 企業使用者加入企業的審核狀態 status_reason TEXT, -- 審核不過原因等 -- 企業內部細粒度權限 (範例,可擴展) can_post_articles BOOLEAN DEFAULT FALSE, -- 是否能發布經驗分享文章 (由企業後台管理者控制) can_edit_activities BOOLEAN DEFAULT FALSE, -- 是否能編輯活動資料 (由企業後台管理者控制) -- 移除 can_ask_questions 欄位,因為提問應該是所有用戶的基本權限 -- can_manage_specific_activity_id UUID, -- 管理特定活動的權限 joined_at TIMESTAMP WITH TIME ZONE, -- 審核通過加入時間 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- 申請時間 updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (organization_id, user_id) ); -- 索引 CREATE INDEX idx_organizations_name ON organizations(name); CREATE INDEX idx_organizations_admin_user_id ON organizations(admin_user_id); CREATE INDEX idx_organizations_status ON organizations(status); CREATE INDEX idx_organizations_industry ON organizations(industry); CREATE INDEX idx_organization_members_user_id ON organization_members(user_id); ``` ## 怎麼起在本地 路徑要改 ### docker-compose.yml ``` version: '3.8' services: postgres_db: image: postgres:15 container_name: impact_copilot_postgres_demo environment: POSTGRES_USER: ricky_user POSTGRES_PASSWORD: ricky_secret POSTGRES_DB: main_db # 這個資料庫會被自動建立,init-script.sh 會用它來連線並建立其他DB ports: - "5433:5432" volumes: - pg_data_demo:/var/lib/postgresql/data - ../db-v1:/docker-entrypoint-initdb.d/schemas - ./init-db/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U ricky_user -d main_db"] interval: 10s timeout: 5s retries: 5 pgadmin: image: dpage/pgadmin4 container_name: impact_copilot_pgadmin environment: PGADMIN_DEFAULT_EMAIL: ricky.chen@infinirc.com # pgAdmin 登入 Email PGADMIN_DEFAULT_PASSWORD: admin # pgAdmin 登入密碼 ports: - "5050:80" # pgAdmin on http://localhost:5050 volumes: - pgadmin_data:/var/lib/pgadmin depends_on: postgres_db: condition: service_healthy restart: unless-stopped volumes: pg_data_demo: pgadmin_data: ``` ### demo-api/init-db/init-script.sh ``` #!/bin/bash set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE DATABASE member_service_db; CREATE DATABASE organization_service_db; CREATE DATABASE content_service_db; CREATE DATABASE activity_service_db; EOSQL echo "資料庫 member_service_db, organization_service_db, content_service_db, activity_service_db 已建立。" # 匯入 Schema echo "Member Service Schema..." psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "member_service_db" < /docker-entrypoint-initdb.d/schemas/Member-Service-Database.sql echo " Organization Service Schema..." psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "organization_service_db" < /docker-entrypoint-initdb.d/schemas/Organization-Service-Database.sql echo " Content Service Schema..." psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "content_service_db" < /docker-entrypoint-initdb.d/schemas/Content-Service-Database.sql echo " Activity Service Schema..." psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "activity_service_db" < /docker-entrypoint-initdb.d/schemas/Activity-Service-Database.sql echo "應該成了" ```