-- ==================================== -- HR Portal Database Schema -- ==================================== -- 創建時間: 2026-02-08 -- 資料庫: PostgreSQL 16 -- ==================================== -- 設定時區 SET timezone = 'Asia/Taipei'; -- ==================================== -- 1. 事業部表 (Business Units) -- ==================================== CREATE TABLE IF NOT EXISTS business_units ( id SERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, name_en VARCHAR(100), description TEXT, manager_id INTEGER, -- 將在建立 employees 表後設定外鍵 email_domain VARCHAR(50), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 初始資料 INSERT INTO business_units (code, name, name_en, email_domain, description) VALUES ('wind-energy', '玄鐵風能授權服務事業部', 'Wind Energy Licensing', 'ease.taipei', '風力發電技術授權與風場評估服務'), ('carbon-credit', '國際碳權申請服務事業部', 'Carbon Credit Services', 'ease.taipei', '碳權申請、碳盤查與碳交易媒合服務'), ('smart-rd', '智能研發服務事業部', 'Smart R&D Services', 'lab.taipei', 'AI/ML、IoT 與能源管理系統研發'), ('management', '管理部門', 'Management', 'porscheworld.tw', '人資、財務、行政、資訊等管理部門') ON CONFLICT (code) DO NOTHING; -- ==================================== -- 2. 部門表 (Divisions) -- ==================================== CREATE TABLE IF NOT EXISTS divisions ( id SERIAL PRIMARY KEY, business_unit_id INTEGER REFERENCES business_units(id) ON DELETE CASCADE, code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, name_en VARCHAR(100), manager_id INTEGER, -- 將在建立 employees 表後設定外鍵 email VARCHAR(100), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 初始資料 - 玄鐵風能授權服務 INSERT INTO divisions (business_unit_id, code, name, name_en, email) VALUES (1, 'wind-licensing', '技術授權部', 'Technical Licensing', 'wind-licensing@ease.taipei'), (1, 'wind-assessment', '風場評估部', 'Wind Assessment', 'wind-assessment@ease.taipei'), (1, 'wind-service', '客戶服務部', 'Customer Service', 'wind-service@ease.taipei') ON CONFLICT (code) DO NOTHING; -- 國際碳權申請服務 INSERT INTO divisions (business_unit_id, code, name, name_en, email) VALUES (2, 'carbon-apply', '碳權申請部', 'Carbon Application', 'carbon-apply@ease.taipei'), (2, 'carbon-audit', '碳盤查部', 'Carbon Audit', 'carbon-audit@ease.taipei'), (2, 'carbon-trade', '碳交易部', 'Carbon Trading', 'carbon-trade@ease.taipei') ON CONFLICT (code) DO NOTHING; -- 智能研發服務 INSERT INTO divisions (business_unit_id, code, name, name_en, email) VALUES (3, 'software-dev', '軟體研發部', 'Software Development', 'software@lab.taipei'), (3, 'hardware-dev', '硬體研發部', 'Hardware Development', 'hardware@lab.taipei'), (3, 'product-mgmt', '產品管理部', 'Product Management', 'product@lab.taipei') ON CONFLICT (code) DO NOTHING; -- 管理部門 INSERT INTO divisions (business_unit_id, code, name, name_en, email) VALUES (4, 'hr', '人力資源部', 'Human Resources', 'hr@porscheworld.tw'), (4, 'finance', '財務部', 'Finance', 'finance@porscheworld.tw'), (4, 'admin', '行政部', 'Administration', 'admin@porscheworld.tw'), (4, 'it', '資訊部', 'Information Technology', 'it@porscheworld.tw') ON CONFLICT (code) DO NOTHING; -- ==================================== -- 3. 員工表 (Employees) -- ==================================== CREATE TABLE IF NOT EXISTS employees ( id SERIAL PRIMARY KEY, keycloak_user_id UUID UNIQUE, employee_id VARCHAR(20) UNIQUE NOT NULL, username VARCHAR(100) UNIQUE NOT NULL, -- 基本資料 first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, chinese_name VARCHAR(50), email VARCHAR(255) UNIQUE NOT NULL, phone VARCHAR(20), mobile VARCHAR(20), -- 任職資訊 business_unit_id INTEGER REFERENCES business_units(id), division_id INTEGER REFERENCES divisions(id), team VARCHAR(100), position VARCHAR(100), job_title VARCHAR(100), job_level VARCHAR(20), -- C-Level, VP, Director, Manager, Senior, Staff, etc. employment_type VARCHAR(20) DEFAULT 'full-time', -- full-time, part-time, contractor, intern -- 日期 hire_date DATE, termination_date DATE, -- 狀態 status VARCHAR(20) DEFAULT 'active', -- active, inactive, suspended, terminated -- 審計 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by VARCHAR(100), updated_by VARCHAR(100) ); -- 索引 CREATE INDEX idx_employees_keycloak_id ON employees(keycloak_user_id); CREATE INDEX idx_employees_status ON employees(status); CREATE INDEX idx_employees_business_unit ON employees(business_unit_id); CREATE INDEX idx_employees_division ON employees(division_id); CREATE INDEX idx_employees_email ON employees(email); -- 新增外鍵到 business_units 和 divisions ALTER TABLE business_units ADD CONSTRAINT fk_business_unit_manager FOREIGN KEY (manager_id) REFERENCES employees(id) ON DELETE SET NULL; ALTER TABLE divisions ADD CONSTRAINT fk_division_manager FOREIGN KEY (manager_id) REFERENCES employees(id) ON DELETE SET NULL; -- ==================================== -- 4. 郵件帳號表 (Email Accounts) -- ==================================== CREATE TABLE IF NOT EXISTS email_accounts ( id SERIAL PRIMARY KEY, employee_id INTEGER REFERENCES employees(id) ON DELETE CASCADE, email_address VARCHAR(255) UNIQUE NOT NULL, mailbox_quota_mb INTEGER DEFAULT 1024, mailbox_used_mb INTEGER DEFAULT 0, -- 郵件設定 forward_to VARCHAR(255), auto_reply BOOLEAN DEFAULT FALSE, auto_reply_message TEXT, -- 狀態 is_active BOOLEAN DEFAULT TRUE, -- 審計 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by VARCHAR(100) ); CREATE INDEX idx_email_accounts_employee ON email_accounts(employee_id); CREATE INDEX idx_email_accounts_email ON email_accounts(email_address); -- ==================================== -- 5. 網路硬碟表 (Network Drives) -- ==================================== CREATE TABLE IF NOT EXISTS network_drives ( id SERIAL PRIMARY KEY, employee_id INTEGER REFERENCES employees(id) ON DELETE CASCADE, -- 硬碟資訊 drive_name VARCHAR(100) NOT NULL, drive_path VARCHAR(500) NOT NULL, quota_gb INTEGER DEFAULT 10, used_gb DECIMAL(10,2) DEFAULT 0, -- 存取設定 webdav_url VARCHAR(500), smb_path VARCHAR(500), -- 權限 can_share BOOLEAN DEFAULT FALSE, -- 狀態 is_active BOOLEAN DEFAULT TRUE, -- 審計 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_network_drives_employee ON network_drives(employee_id); -- ==================================== -- 6. 系統權限表 (System Permissions) -- ==================================== CREATE TABLE IF NOT EXISTS system_permissions ( id SERIAL PRIMARY KEY, employee_id INTEGER REFERENCES employees(id) ON DELETE CASCADE, system_name VARCHAR(100) NOT NULL, system_url VARCHAR(500), access_level VARCHAR(50), -- admin, user, readonly -- 狀態 is_active BOOLEAN DEFAULT TRUE, granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, granted_by VARCHAR(100), revoked_at TIMESTAMP, revoked_by VARCHAR(100), UNIQUE(employee_id, system_name) ); CREATE INDEX idx_system_permissions_employee ON system_permissions(employee_id); CREATE INDEX idx_system_permissions_system ON system_permissions(system_name); -- ==================================== -- 7. 專案表 (Projects) -- ==================================== CREATE TABLE IF NOT EXISTS projects ( id SERIAL PRIMARY KEY, business_unit_id INTEGER REFERENCES business_units(id), project_code VARCHAR(50) UNIQUE NOT NULL, project_name VARCHAR(200) NOT NULL, description TEXT, client_name VARCHAR(200), -- 專案狀態 status VARCHAR(20) DEFAULT 'planning', -- planning, active, on-hold, completed, cancelled -- 專案經理 project_manager_id INTEGER REFERENCES employees(id), -- 時間 start_date DATE, end_date DATE, actual_end_date DATE, -- 預算 budget_amount DECIMAL(15,2), budget_currency VARCHAR(10) DEFAULT 'TWD', -- 專案空間 nas_path VARCHAR(500), git_repo VARCHAR(200), -- 審計 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by VARCHAR(100) ); CREATE INDEX idx_projects_business_unit ON projects(business_unit_id); CREATE INDEX idx_projects_status ON projects(status); CREATE INDEX idx_projects_manager ON projects(project_manager_id); -- ==================================== -- 8. 專案成員表 (Project Members) -- ==================================== CREATE TABLE IF NOT EXISTS project_members ( id SERIAL PRIMARY KEY, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, employee_id INTEGER REFERENCES employees(id) ON DELETE CASCADE, role VARCHAR(50), -- project-manager, developer, consultant, support allocation_percentage INTEGER DEFAULT 100, -- 0-100 joined_date DATE DEFAULT CURRENT_DATE, left_date DATE, UNIQUE(project_id, employee_id) ); CREATE INDEX idx_project_members_project ON project_members(project_id); CREATE INDEX idx_project_members_employee ON project_members(employee_id); -- ==================================== -- 9. 審計日誌表 (Audit Logs) -- ==================================== CREATE TABLE IF NOT EXISTS audit_logs ( id SERIAL PRIMARY KEY, employee_id INTEGER REFERENCES employees(id), action VARCHAR(50) NOT NULL, -- create, update, delete, login, logout resource_type VARCHAR(50), -- employee, email, drive, project resource_id INTEGER, old_value JSONB, new_value JSONB, ip_address VARCHAR(50), user_agent TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_audit_logs_employee ON audit_logs(employee_id); CREATE INDEX idx_audit_logs_action ON audit_logs(action); CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id); CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at DESC); -- ==================================== -- 10. 更新時間觸發器函數 -- ==================================== 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_business_units_updated_at BEFORE UPDATE ON business_units FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_divisions_updated_at BEFORE UPDATE ON divisions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_employees_updated_at BEFORE UPDATE ON employees FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_email_accounts_updated_at BEFORE UPDATE ON email_accounts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_network_drives_updated_at BEFORE UPDATE ON network_drives FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_projects_updated_at BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ==================================== -- 11. 建立視圖 (Views) -- ==================================== -- 員工完整資訊視圖 CREATE OR REPLACE VIEW v_employees_full AS SELECT e.*, bu.name as business_unit_name, bu.name_en as business_unit_name_en, d.name as division_name, d.name_en as division_name_en FROM employees e LEFT JOIN business_units bu ON e.business_unit_id = bu.id LEFT JOIN divisions d ON e.division_id = d.id; -- 專案人力統計視圖 CREATE OR REPLACE VIEW v_project_stats AS SELECT p.id, p.project_code, p.project_name, p.status, COUNT(pm.id) as member_count, SUM(pm.allocation_percentage) as total_allocation FROM projects p LEFT JOIN project_members pm ON p.id = pm.project_id AND pm.left_date IS NULL GROUP BY p.id, p.project_code, p.project_name, p.status; -- 部門人力統計視圖 CREATE OR REPLACE VIEW v_division_headcount AS SELECT bu.name as business_unit_name, d.name as division_name, COUNT(e.id) as employee_count, COUNT(CASE WHEN e.status = 'active' THEN 1 END) as active_count FROM divisions d LEFT JOIN business_units bu ON d.business_unit_id = bu.id LEFT JOIN employees e ON e.division_id = d.id GROUP BY bu.name, d.name; -- ==================================== -- 完成 -- ==================================== -- 授予權限 (依實際用戶名調整) -- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO hr_user; -- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO hr_user; SELECT 'HR Portal Database Schema 初始化完成!' as message;