feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
Major Features: - ✅ Multi-tenant architecture (tenant isolation) - ✅ Employee CRUD with lifecycle management (onboarding/offboarding) - ✅ Department tree structure with email domain management - ✅ Company info management (single-record editing) - ✅ System functions CRUD (permission management) - ✅ Email account management (multi-account per employee) - ✅ Keycloak SSO integration (auth.lab.taipei) - ✅ Redis session storage (10.1.0.254:6379) - Solves Cookie 4KB limitation - Cross-system session sharing - Sliding expiration (8 hours) - Automatic token refresh Technical Stack: Backend: - FastAPI + SQLAlchemy - PostgreSQL 16 (10.1.0.20:5433) - Keycloak Admin API integration - Docker Mailserver integration (SSH) - Alembic migrations Frontend: - Next.js 14 (App Router) - NextAuth 4 with Keycloak Provider - Redis session storage (ioredis) - Tailwind CSS Infrastructure: - Redis 7 (10.1.0.254:6379) - Session + Cache - Keycloak 26.1.0 (auth.lab.taipei) - Docker Mailserver (10.1.0.254) Architecture Highlights: - Session管理由 Keycloak + Redis 統一控制 - 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session - Token 自動刷新,異質服務整合 - 未來可無縫遷移到雲端 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
378
scripts/init-db.sql
Normal file
378
scripts/init-db.sql
Normal file
@@ -0,0 +1,378 @@
|
||||
-- ====================================
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user