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:
165
database/README.md
Normal file
165
database/README.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# HR Portal 資料庫設計
|
||||
|
||||
## 📋 Schema 版本
|
||||
|
||||
- **版本**: v2.0
|
||||
- **創建日期**: 2026-02-10
|
||||
- **設計依據**: [員工多身份設計文件.md](../../../2.專案設計區/4.HR_Portal/員工多身份設計文件.md)
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ 資料表結構
|
||||
|
||||
### 核心表格
|
||||
|
||||
1. **employees** - 員工基本資料
|
||||
2. **business_units** - 事業部
|
||||
3. **departments** - 部門
|
||||
4. **employee_identities** - 員工身份 (多對多關係)
|
||||
5. **network_drives** - 網路硬碟 (一對一關係)
|
||||
6. **audit_logs** - 審計日誌
|
||||
|
||||
### 關聯圖
|
||||
|
||||
```
|
||||
employees (員工)
|
||||
│
|
||||
├──< employee_identities (身份) ──> business_units (事業部)
|
||||
│ ──> departments (部門)
|
||||
│
|
||||
└──< network_drives (NAS 帳號)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速開始
|
||||
|
||||
### 方法一: Docker Compose (推薦)
|
||||
|
||||
```bash
|
||||
# 進入 database 目錄
|
||||
cd W:\DevOps-Workspace\3.Develop\4.HR_Portal\database
|
||||
|
||||
# 啟動資料庫
|
||||
docker-compose up -d
|
||||
|
||||
# 執行測試腳本
|
||||
docker-compose exec postgres psql -U hr_admin -d hr_portal -f /test_schema.sql
|
||||
|
||||
# 或從本地執行
|
||||
docker exec -i hr-portal-db-test psql -U hr_admin -d hr_portal < test_schema.sql
|
||||
```
|
||||
|
||||
詳細測試說明請參考 [TESTING.md](./TESTING.md)
|
||||
|
||||
### 方法二: 本地 PostgreSQL
|
||||
|
||||
```bash
|
||||
# 1. 創建資料庫
|
||||
createdb hr_portal
|
||||
|
||||
# 或使用 psql
|
||||
psql -U postgres -c "CREATE DATABASE hr_portal;"
|
||||
|
||||
# 2. 執行 Schema
|
||||
psql -U postgres -d hr_portal -f schema.sql
|
||||
|
||||
# 3. 執行測試
|
||||
psql -U postgres -d hr_portal -f test_schema.sql
|
||||
```
|
||||
|
||||
### 驗證
|
||||
|
||||
```bash
|
||||
# 檢查表格
|
||||
psql -U postgres -d hr_portal -c "\dt"
|
||||
|
||||
# 檢查視圖
|
||||
psql -U postgres -d hr_portal -c "\dv"
|
||||
|
||||
# 查看初始資料
|
||||
psql -U postgres -d hr_portal -c "SELECT * FROM business_units;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 重要設計概念
|
||||
|
||||
### 員工多身份
|
||||
|
||||
一個員工 (`employees`) 可以有多個身份 (`employee_identities`),每個身份對應一個事業部:
|
||||
|
||||
- **同事業部多部門**: 共用一個 SSO 帳號
|
||||
- **跨事業部**: 獨立的 SSO 帳號
|
||||
|
||||
### SSO 帳號命名
|
||||
|
||||
```
|
||||
格式: {username_base}@{email_domain}
|
||||
|
||||
範例:
|
||||
- porsche.chen@lab.taipei (智能發展部)
|
||||
- porsche.chen@ease.taipei (業務發展部)
|
||||
- porsche.chen@porscheworld.tw (營運管理部)
|
||||
```
|
||||
|
||||
### NAS 帳號
|
||||
|
||||
- 一個員工只有一個 NAS 帳號
|
||||
- 帳號名稱 = `username_base`
|
||||
- 配額由最高職級決定
|
||||
|
||||
---
|
||||
|
||||
## 🔧 開發工具
|
||||
|
||||
### Docker Compose
|
||||
|
||||
提供完整的測試環境,包含 PostgreSQL 16 和 pgAdmin 4。
|
||||
|
||||
**啟動**:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
**訪問 pgAdmin**: http://localhost:5050
|
||||
- Email: admin@lab.taipei
|
||||
- Password: admin
|
||||
|
||||
**資料庫連線資訊**:
|
||||
- Host: postgres (Docker 內部) / localhost (外部)
|
||||
- Port: 5433 (外部) / 5432 (內部)
|
||||
- Database: hr_portal
|
||||
- User: hr_admin
|
||||
- Password: hr_dev_password_2026
|
||||
|
||||
### 測試腳本
|
||||
|
||||
提供完整的測試腳本 `test_schema.sql`,包含:
|
||||
- 表格結構驗證
|
||||
- 外鍵約束測試
|
||||
- 唯一約束測試
|
||||
- 索引檢查
|
||||
- 模擬資料插入
|
||||
|
||||
詳見 [TESTING.md](./TESTING.md)
|
||||
|
||||
### 遷移工具
|
||||
|
||||
建議使用 Alembic 進行資料庫遷移管理。
|
||||
|
||||
---
|
||||
|
||||
## 📖 相關文檔
|
||||
|
||||
- [TESTING.md](./TESTING.md) - 資料庫測試指南
|
||||
- [員工多身份設計文件](../../../2.專案設計區/4.HR_Portal/員工多身份設計文件.md)
|
||||
- [HR Portal 設計文件](../../../2.專案設計區/4.HR_Portal/HR Portal設計文件.md)
|
||||
|
||||
## 📂 檔案清單
|
||||
|
||||
- `schema.sql` - 資料庫 Schema (v2.0)
|
||||
- `test_schema.sql` - 測試腳本
|
||||
- `docker-compose.yml` - Docker 測試環境
|
||||
- `TESTING.md` - 測試指南
|
||||
- `README.md` - 本文件
|
||||
270
database/TESTING.md
Normal file
270
database/TESTING.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# HR Portal 資料庫測試指南
|
||||
|
||||
## 🎯 測試目的
|
||||
|
||||
驗證資料庫 schema 是否正確創建,包括:
|
||||
- 表格結構
|
||||
- 外鍵約束
|
||||
- 唯一約束
|
||||
- 索引
|
||||
- 初始資料
|
||||
- 視圖
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速測試 (使用 Docker)
|
||||
|
||||
### 1. 啟動測試資料庫
|
||||
|
||||
```bash
|
||||
cd W:\DevOps-Workspace\3.Develop\4.HR_Portal\database
|
||||
|
||||
# 啟動 PostgreSQL 和 pgAdmin
|
||||
docker-compose up -d
|
||||
|
||||
# 等待資料庫初始化完成 (約 10 秒)
|
||||
docker-compose logs -f postgres
|
||||
```
|
||||
|
||||
資料庫會自動執行 `schema.sql` 進行初始化。
|
||||
|
||||
### 2. 執行測試腳本
|
||||
|
||||
```bash
|
||||
# 執行測試
|
||||
docker exec -i hr-portal-db-test psql -U hr_admin -d hr_portal < test_schema.sql
|
||||
|
||||
# 或使用 Docker Compose
|
||||
docker-compose exec postgres psql -U hr_admin -d hr_portal -f /test_schema.sql
|
||||
```
|
||||
|
||||
### 3. 查看測試結果
|
||||
|
||||
測試腳本會輸出以下資訊:
|
||||
- ✓ 表格是否存在
|
||||
- ✓ 視圖是否存在
|
||||
- ✓ 初始資料是否正確
|
||||
- ✓ 外鍵約束
|
||||
- ✓ 唯一約束
|
||||
- ✓ 索引
|
||||
- ✓ 模擬員工資料插入
|
||||
- ✓ 唯一約束測試
|
||||
|
||||
### 4. 使用 pgAdmin 查看
|
||||
|
||||
訪問 http://localhost:5050
|
||||
|
||||
- **Email**: admin@lab.taipei
|
||||
- **Password**: admin
|
||||
|
||||
添加伺服器:
|
||||
- **Name**: HR Portal Test
|
||||
- **Host**: postgres
|
||||
- **Port**: 5432
|
||||
- **Database**: hr_portal
|
||||
- **Username**: hr_admin
|
||||
- **Password**: hr_dev_password_2026
|
||||
|
||||
### 5. 停止測試環境
|
||||
|
||||
```bash
|
||||
# 停止容器 (保留資料)
|
||||
docker-compose stop
|
||||
|
||||
# 停止並刪除容器 (保留資料)
|
||||
docker-compose down
|
||||
|
||||
# 完全清理 (刪除資料和容器)
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 手動測試 (本地 PostgreSQL)
|
||||
|
||||
### 前置需求
|
||||
|
||||
- PostgreSQL 16 已安裝
|
||||
- 已創建資料庫使用者
|
||||
|
||||
### 1. 創建資料庫
|
||||
|
||||
```bash
|
||||
# 使用 createdb
|
||||
createdb -U postgres hr_portal
|
||||
|
||||
# 或使用 psql
|
||||
psql -U postgres -c "CREATE DATABASE hr_portal;"
|
||||
```
|
||||
|
||||
### 2. 執行 Schema
|
||||
|
||||
```bash
|
||||
psql -U postgres -d hr_portal -f schema.sql
|
||||
```
|
||||
|
||||
### 3. 執行測試
|
||||
|
||||
```bash
|
||||
psql -U postgres -d hr_portal -f test_schema.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 測試項目說明
|
||||
|
||||
### 1. 表格存在性測試
|
||||
|
||||
檢查以下 6 個核心表格是否創建:
|
||||
- `employees` - 員工基本資料
|
||||
- `business_units` - 事業部
|
||||
- `departments` - 部門
|
||||
- `employee_identities` - 員工身份
|
||||
- `network_drives` - 網路硬碟
|
||||
- `audit_logs` - 審計日誌
|
||||
|
||||
### 2. 視圖測試
|
||||
|
||||
檢查 `v_employee_full_info` 視圖是否創建。
|
||||
|
||||
### 3. 初始資料測試
|
||||
|
||||
驗證初始資料是否正確插入:
|
||||
- 3 個事業部
|
||||
- 9 個部門
|
||||
|
||||
### 4. 外鍵約束測試
|
||||
|
||||
檢查以下外鍵約束:
|
||||
- `departments.business_unit_id` → `business_units.id`
|
||||
- `employee_identities.employee_id` → `employees.id`
|
||||
- `employee_identities.business_unit_id` → `business_units.id`
|
||||
- `employee_identities.department_id` → `departments.id`
|
||||
- `network_drives.employee_id` → `employees.id`
|
||||
|
||||
### 5. 唯一約束測試
|
||||
|
||||
檢查以下唯一約束:
|
||||
- `employees.employee_id` - 員工編號唯一
|
||||
- `employees.username_base` - 基礎帳號唯一
|
||||
- `business_units.code` - 事業部代碼唯一
|
||||
- `business_units.email_domain` - 郵件網域唯一
|
||||
- `employee_identities.username` - SSO 帳號唯一
|
||||
- `employee_identities.keycloak_id` - Keycloak UUID 唯一
|
||||
- `employee_identities(employee_id, business_unit_id)` - 一個員工在同一事業部只能有一個身份
|
||||
- `network_drives.drive_name` - NAS 帳號名稱唯一
|
||||
- `network_drives.employee_id` - 一個員工只有一個 NAS 帳號
|
||||
|
||||
### 6. 索引測試
|
||||
|
||||
檢查所有索引是否創建,包括:
|
||||
- 主鍵索引
|
||||
- 外鍵索引
|
||||
- 查詢優化索引
|
||||
|
||||
### 7. 模擬資料插入測試
|
||||
|
||||
模擬真實場景插入資料:
|
||||
1. 創建員工 (porsche.chen)
|
||||
2. 創建第一個身份 (智能發展部 - 資訊部)
|
||||
3. 創建第二個身份 (業務發展部 - 顧問部)
|
||||
4. 創建 NAS 帳號
|
||||
5. 查詢視圖驗證資料
|
||||
6. 回滾 (不影響資料庫)
|
||||
|
||||
### 8. 約束違反測試
|
||||
|
||||
測試唯一約束是否生效:
|
||||
- 嘗試為同一員工在同一事業部創建兩個身份 (應該失敗)
|
||||
- 驗證錯誤訊息
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常見問題
|
||||
|
||||
### Q1: 測試腳本執行失敗
|
||||
|
||||
**錯誤**: `psql: error: connection to server failed`
|
||||
|
||||
**解決**:
|
||||
```bash
|
||||
# 檢查 PostgreSQL 是否運行
|
||||
docker-compose ps
|
||||
|
||||
# 查看日誌
|
||||
docker-compose logs postgres
|
||||
```
|
||||
|
||||
### Q2: Schema 初始化失敗
|
||||
|
||||
**錯誤**: `relation "xxx" already exists`
|
||||
|
||||
**解決**:
|
||||
```bash
|
||||
# 完全重置資料庫
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Q3: 無法連線到 pgAdmin
|
||||
|
||||
**錯誤**: `Unable to connect to server`
|
||||
|
||||
**解決**:
|
||||
- 確認 pgAdmin 容器運行: `docker-compose ps`
|
||||
- 確認使用 `postgres` (容器名稱) 作為 Host,而非 `localhost`
|
||||
|
||||
---
|
||||
|
||||
## 📊 預期輸出
|
||||
|
||||
測試成功時應該看到:
|
||||
|
||||
```
|
||||
=========================================
|
||||
HR Portal Database Schema 測試
|
||||
=========================================
|
||||
|
||||
1. 檢查表格是否存在...
|
||||
|
||||
table_name | status
|
||||
--------------------+--------
|
||||
audit_logs | ✓ 存在
|
||||
business_units | ✓ 存在
|
||||
departments | ✓ 存在
|
||||
employee_identities| ✓ 存在
|
||||
employees | ✓ 存在
|
||||
network_drives | ✓ 存在
|
||||
|
||||
2. 檢查視圖是否存在...
|
||||
|
||||
view_name | status
|
||||
------------------------+--------
|
||||
v_employee_full_info | ✓ 存在
|
||||
|
||||
...
|
||||
|
||||
✓ 唯一約束測試成功: 正確阻止重複身份
|
||||
|
||||
=========================================
|
||||
測試完成!
|
||||
=========================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 下一步
|
||||
|
||||
測試通過後,可以進行:
|
||||
|
||||
1. **開發後端 API** - 創建 FastAPI 專案
|
||||
2. **建立 ORM 模型** - SQLAlchemy 模型定義
|
||||
3. **整合測試** - 後端 API 與資料庫整合測試
|
||||
|
||||
---
|
||||
|
||||
## 📖 相關文件
|
||||
|
||||
- [schema.sql](./schema.sql) - 資料庫 Schema
|
||||
- [README.md](./README.md) - 資料庫設計說明
|
||||
- [員工多身份設計文件](../../../2.專案設計區/4.HR_Portal/員工多身份設計文件.md)
|
||||
44
database/docker-compose.yml
Normal file
44
database/docker-compose.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: hr-portal-db-test
|
||||
environment:
|
||||
POSTGRES_DB: hr_portal
|
||||
POSTGRES_USER: hr_admin
|
||||
POSTGRES_PASSWORD: hr_dev_password_2026
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
|
||||
ports:
|
||||
- "5433:5432" # 使用 5433 避免與其他 PostgreSQL 實例衝突
|
||||
volumes:
|
||||
- hr_portal_db_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- hr-portal-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U hr_admin -d hr_portal"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:latest
|
||||
container_name: hr-portal-pgadmin
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@lab.taipei
|
||||
PGADMIN_DEFAULT_PASSWORD: admin
|
||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||
ports:
|
||||
- "5050:80"
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- hr-portal-network
|
||||
|
||||
volumes:
|
||||
hr_portal_db_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
hr-portal-network:
|
||||
driver: bridge
|
||||
229
database/schema.sql
Normal file
229
database/schema.sql
Normal file
@@ -0,0 +1,229 @@
|
||||
-- ============================================================================
|
||||
-- HR Portal Database Schema v2.0
|
||||
-- 設計文件: 員工多身份設計文件.md
|
||||
-- 創建日期: 2026-02-10
|
||||
-- ============================================================================
|
||||
|
||||
-- 清理現有表格 (開發環境用)
|
||||
DROP TABLE IF EXISTS audit_logs CASCADE;
|
||||
DROP TABLE IF EXISTS network_drives CASCADE;
|
||||
DROP TABLE IF EXISTS employee_identities CASCADE;
|
||||
DROP TABLE IF EXISTS departments CASCADE;
|
||||
DROP TABLE IF EXISTS business_units CASCADE;
|
||||
DROP TABLE IF EXISTS employees CASCADE;
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. employees (員工基本資料)
|
||||
-- ============================================================================
|
||||
CREATE TABLE employees (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id VARCHAR(20) UNIQUE NOT NULL, -- EMP001
|
||||
username_base VARCHAR(50) UNIQUE NOT NULL, -- porsche.chen (全公司唯一)
|
||||
legal_name VARCHAR(100) NOT NULL, -- 法定姓名: 陳保時
|
||||
english_name VARCHAR(100), -- 英文名: Porsche Chen
|
||||
phone VARCHAR(20),
|
||||
mobile VARCHAR(20),
|
||||
hire_date DATE NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'active', -- active/inactive/terminated
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_employees_status ON employees(status);
|
||||
CREATE INDEX idx_employees_username_base ON employees(username_base);
|
||||
|
||||
COMMENT ON TABLE employees IS '員工基本資料 - 一個員工的核心資訊';
|
||||
COMMENT ON COLUMN employees.username_base IS '基礎帳號名稱,全公司唯一,用於生成各事業部 SSO 帳號';
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. business_units (事業部)
|
||||
-- ============================================================================
|
||||
CREATE TABLE business_units (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL, -- 業務發展部
|
||||
name_en VARCHAR(100), -- Business Development
|
||||
code VARCHAR(20) UNIQUE NOT NULL, -- biz
|
||||
email_domain VARCHAR(100) UNIQUE NOT NULL, -- ease.taipei
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE business_units IS '事業部 - 公司的一級組織單位';
|
||||
COMMENT ON COLUMN business_units.email_domain IS '該事業部的郵件網域 (用於生成 SSO 帳號和郵件地址)';
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. departments (部門)
|
||||
-- ============================================================================
|
||||
CREATE TABLE departments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
business_unit_id INTEGER NOT NULL REFERENCES business_units(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(business_unit_id, code)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_departments_business_unit ON departments(business_unit_id);
|
||||
|
||||
COMMENT ON TABLE departments IS '部門 - 隸屬於事業部的二級組織單位';
|
||||
COMMENT ON COLUMN departments.code IS '部門代碼,在同一事業部內唯一';
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. employee_identities (員工身份)
|
||||
-- ============================================================================
|
||||
CREATE TABLE employee_identities (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id INTEGER NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||
|
||||
-- SSO 帳號 (= 郵件地址)
|
||||
username VARCHAR(100) UNIQUE NOT NULL, -- porsche.chen@lab.taipei
|
||||
keycloak_id VARCHAR(100) UNIQUE NOT NULL, -- Keycloak UUID
|
||||
|
||||
-- 組織與職務
|
||||
business_unit_id INTEGER NOT NULL REFERENCES business_units(id),
|
||||
department_id INTEGER REFERENCES departments(id),
|
||||
job_title VARCHAR(100) NOT NULL, -- 技術總監
|
||||
job_level VARCHAR(20) NOT NULL, -- Junior/Mid/Senior/Manager
|
||||
is_primary BOOLEAN DEFAULT FALSE, -- 是否為主要身份
|
||||
|
||||
-- 郵件配額
|
||||
email_quota_mb INTEGER NOT NULL,
|
||||
|
||||
-- 時間記錄
|
||||
started_at DATE NOT NULL,
|
||||
ended_at DATE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 一個員工在一個事業部只能有一個身份
|
||||
UNIQUE(employee_id, business_unit_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_identities_employee ON employee_identities(employee_id);
|
||||
CREATE INDEX idx_identities_keycloak ON employee_identities(keycloak_id);
|
||||
CREATE INDEX idx_identities_username ON employee_identities(username);
|
||||
CREATE INDEX idx_identities_business_unit ON employee_identities(business_unit_id);
|
||||
|
||||
COMMENT ON TABLE employee_identities IS '員工身份 - 一個員工在某個事業部的身份資訊';
|
||||
COMMENT ON COLUMN employee_identities.username IS 'SSO 帳號 = 郵件地址 (格式: username_base@email_domain)';
|
||||
COMMENT ON CONSTRAINT employee_identities_employee_id_business_unit_id_key ON employee_identities IS '一個員工在同一事業部只能有一個身份';
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. network_drives (網路硬碟)
|
||||
-- ============================================================================
|
||||
CREATE TABLE network_drives (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id INTEGER NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||
|
||||
-- 一個員工只有一個 NAS 帳號
|
||||
drive_name VARCHAR(100) UNIQUE NOT NULL, -- porsche.chen (與 username_base 相同)
|
||||
quota_gb INTEGER NOT NULL, -- 200 (依最高職級決定)
|
||||
webdav_url VARCHAR(255),
|
||||
smb_url VARCHAR(255),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(employee_id) -- 一對一關係
|
||||
);
|
||||
|
||||
CREATE INDEX idx_network_drives_employee ON network_drives(employee_id);
|
||||
|
||||
COMMENT ON TABLE network_drives IS '網路硬碟 - 一個員工對應一個 NAS 帳號';
|
||||
COMMENT ON COLUMN network_drives.quota_gb IS 'NAS 配額 (GB),取該員工所有身份中的最高職級決定';
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. audit_logs (審計日誌)
|
||||
-- ============================================================================
|
||||
CREATE TABLE audit_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
action VARCHAR(50) NOT NULL, -- create/update/delete/login
|
||||
resource_type VARCHAR(50) NOT NULL, -- employee/identity/department
|
||||
resource_id INTEGER,
|
||||
performed_by VARCHAR(100) NOT NULL, -- 操作者的 SSO 帳號
|
||||
performed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
details JSONB, -- 詳細變更內容
|
||||
ip_address VARCHAR(45) -- IPv4/IPv6
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
|
||||
CREATE INDEX idx_audit_logs_performed_by ON audit_logs(performed_by);
|
||||
CREATE INDEX idx_audit_logs_performed_at ON audit_logs(performed_at);
|
||||
|
||||
COMMENT ON TABLE audit_logs IS '審計日誌 - 記錄所有關鍵操作,符合 ISO 要求';
|
||||
|
||||
-- ============================================================================
|
||||
-- 初始資料
|
||||
-- ============================================================================
|
||||
|
||||
-- 事業部
|
||||
INSERT INTO business_units (name, name_en, code, email_domain, description) VALUES
|
||||
('業務發展部', 'Business Development', 'biz', 'ease.taipei', '碳權申請諮詢、碳足跡盤查、碳權交易媒合、業務拓展'),
|
||||
('智能發展部', 'Smart Development', 'smart', 'lab.taipei', 'AI/ML 解決方案開發、IoT 智能監控、能源管理系統'),
|
||||
('營運管理部', 'Operations Management', 'ops', 'porscheworld.tw', '行政管理、財務管理、人力資源、基礎設施');
|
||||
|
||||
-- 部門
|
||||
INSERT INTO departments (business_unit_id, name, code, description) VALUES
|
||||
-- 業務發展部 (id=1)
|
||||
(1, '顧問部', 'consulting', '碳權顧問與技術諮詢'),
|
||||
(1, '營運部', 'operations', '業務執行與客戶管理'),
|
||||
(1, '認證部', 'certification', '國際碳權認證申請'),
|
||||
|
||||
-- 智能發展部 (id=2)
|
||||
(2, '資訊部', 'it', '資訊系統開發與維運'),
|
||||
(2, '研發部', 'research', 'AI/ML 技術研發'),
|
||||
(2, '產品部', 'product', '產品設計與管理'),
|
||||
|
||||
-- 營運管理部 (id=3)
|
||||
(3, '行政部', 'admin', '行政庶務與總務管理'),
|
||||
(3, '財務部', 'finance', '財務會計與資金管理'),
|
||||
(3, '人力資源部', 'hr', '人力資源與招募培訓');
|
||||
|
||||
-- ============================================================================
|
||||
-- 視圖 (Views)
|
||||
-- ============================================================================
|
||||
|
||||
-- 員工完整資訊視圖 (包含所有身份)
|
||||
CREATE OR REPLACE VIEW v_employee_full_info AS
|
||||
SELECT
|
||||
e.id AS employee_id,
|
||||
e.employee_id AS emp_no,
|
||||
e.username_base,
|
||||
e.legal_name,
|
||||
e.english_name,
|
||||
e.status AS employee_status,
|
||||
|
||||
ei.id AS identity_id,
|
||||
ei.username AS sso_username,
|
||||
ei.job_title,
|
||||
ei.job_level,
|
||||
ei.is_primary,
|
||||
ei.email_quota_mb,
|
||||
ei.is_active AS identity_active,
|
||||
|
||||
bu.name AS business_unit_name,
|
||||
bu.code AS business_unit_code,
|
||||
bu.email_domain,
|
||||
|
||||
d.name AS department_name,
|
||||
d.code AS department_code,
|
||||
|
||||
nd.drive_name,
|
||||
nd.quota_gb AS nas_quota_gb
|
||||
FROM employees e
|
||||
LEFT JOIN employee_identities ei ON e.id = ei.employee_id
|
||||
LEFT JOIN business_units bu ON ei.business_unit_id = bu.id
|
||||
LEFT JOIN departments d ON ei.department_id = d.id
|
||||
LEFT JOIN network_drives nd ON e.id = nd.employee_id;
|
||||
|
||||
COMMENT ON VIEW v_employee_full_info IS '員工完整資訊視圖 - 包含所有身份、部門和資源資訊';
|
||||
|
||||
-- ============================================================================
|
||||
-- 完成
|
||||
-- ============================================================================
|
||||
SELECT 'HR Portal Database Schema v2.0 created successfully!' AS status;
|
||||
317
database/test_schema.sql
Normal file
317
database/test_schema.sql
Normal file
@@ -0,0 +1,317 @@
|
||||
-- ============================================================================
|
||||
-- HR Portal Database Schema Test Script
|
||||
-- 用途: 驗證資料庫 schema 是否正確創建
|
||||
-- 使用方法: psql -U postgres -d hr_portal -f test_schema.sql
|
||||
-- ============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '========================================='
|
||||
\echo 'HR Portal Database Schema 測試'
|
||||
\echo '========================================='
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. 測試表格是否存在
|
||||
-- ============================================================================
|
||||
\echo '1. 檢查表格是否存在...'
|
||||
\echo ''
|
||||
|
||||
SELECT
|
||||
table_name,
|
||||
CASE
|
||||
WHEN table_name IN ('employees', 'business_units', 'departments',
|
||||
'employee_identities', 'network_drives', 'audit_logs')
|
||||
THEN '✓ 存在'
|
||||
ELSE '✗ 不存在'
|
||||
END AS status
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name;
|
||||
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. 測試視圖是否存在
|
||||
-- ============================================================================
|
||||
\echo '2. 檢查視圖是否存在...'
|
||||
\echo ''
|
||||
|
||||
SELECT
|
||||
table_name AS view_name,
|
||||
'✓ 存在' AS status
|
||||
FROM information_schema.views
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name;
|
||||
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. 測試初始資料是否存在
|
||||
-- ============================================================================
|
||||
\echo '3. 檢查初始資料...'
|
||||
\echo ''
|
||||
|
||||
\echo '事業部資料:'
|
||||
SELECT id, name, code, email_domain FROM business_units ORDER BY id;
|
||||
|
||||
\echo ''
|
||||
\echo '部門資料:'
|
||||
SELECT id, business_unit_id, name, code FROM departments ORDER BY id;
|
||||
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. 測試外鍵約束
|
||||
-- ============================================================================
|
||||
\echo '4. 檢查外鍵約束...'
|
||||
\echo ''
|
||||
|
||||
SELECT
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage AS ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = 'public'
|
||||
ORDER BY tc.table_name, kcu.column_name;
|
||||
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. 測試唯一約束
|
||||
-- ============================================================================
|
||||
\echo '5. 檢查唯一約束...'
|
||||
\echo ''
|
||||
|
||||
SELECT
|
||||
tc.table_name,
|
||||
tc.constraint_name,
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE tc.constraint_type = 'UNIQUE'
|
||||
AND tc.table_schema = 'public'
|
||||
ORDER BY tc.table_name, kcu.column_name;
|
||||
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. 測試索引
|
||||
-- ============================================================================
|
||||
\echo '6. 檢查索引...'
|
||||
\echo ''
|
||||
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename, indexname;
|
||||
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 7. 測試插入員工資料 (模擬真實場景)
|
||||
-- ============================================================================
|
||||
\echo '7. 測試插入員工資料...'
|
||||
\echo ''
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 插入員工
|
||||
INSERT INTO employees (employee_id, username_base, legal_name, english_name, phone, hire_date, status)
|
||||
VALUES ('EMP001', 'porsche.chen', '陳保時', 'Porsche Chen', '02-1234-5678', '2020-01-01', 'active')
|
||||
RETURNING id, employee_id, username_base, legal_name;
|
||||
|
||||
-- 插入第一個身份 (智能發展部 - 資訊部)
|
||||
INSERT INTO employee_identities (
|
||||
employee_id,
|
||||
username,
|
||||
keycloak_id,
|
||||
business_unit_id,
|
||||
department_id,
|
||||
job_title,
|
||||
job_level,
|
||||
is_primary,
|
||||
email_quota_mb,
|
||||
started_at,
|
||||
is_active
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM employees WHERE username_base = 'porsche.chen'),
|
||||
'porsche.chen@lab.taipei',
|
||||
'test-keycloak-uuid-001',
|
||||
(SELECT id FROM business_units WHERE code = 'smart'),
|
||||
(SELECT id FROM departments WHERE code = 'it' AND business_unit_id = (SELECT id FROM business_units WHERE code = 'smart')),
|
||||
'技術總監',
|
||||
'Senior',
|
||||
true,
|
||||
5000,
|
||||
'2020-01-01',
|
||||
true
|
||||
)
|
||||
RETURNING id, username, job_title, is_primary;
|
||||
|
||||
-- 插入第二個身份 (業務發展部 - 顧問部)
|
||||
INSERT INTO employee_identities (
|
||||
employee_id,
|
||||
username,
|
||||
keycloak_id,
|
||||
business_unit_id,
|
||||
department_id,
|
||||
job_title,
|
||||
job_level,
|
||||
is_primary,
|
||||
email_quota_mb,
|
||||
started_at,
|
||||
is_active
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM employees WHERE username_base = 'porsche.chen'),
|
||||
'porsche.chen@ease.taipei',
|
||||
'test-keycloak-uuid-002',
|
||||
(SELECT id FROM business_units WHERE code = 'biz'),
|
||||
(SELECT id FROM departments WHERE code = 'consulting' AND business_unit_id = (SELECT id FROM business_units WHERE code = 'biz')),
|
||||
'資深顧問',
|
||||
'Senior',
|
||||
false,
|
||||
5000,
|
||||
'2021-06-01',
|
||||
true
|
||||
)
|
||||
RETURNING id, username, job_title, is_primary;
|
||||
|
||||
-- 插入 NAS 帳號
|
||||
INSERT INTO network_drives (
|
||||
employee_id,
|
||||
drive_name,
|
||||
quota_gb,
|
||||
webdav_url,
|
||||
smb_url,
|
||||
is_active
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM employees WHERE username_base = 'porsche.chen'),
|
||||
'porsche.chen',
|
||||
200,
|
||||
'https://nas.lab.taipei/webdav/porsche.chen',
|
||||
'\\10.1.0.30\porsche.chen',
|
||||
true
|
||||
)
|
||||
RETURNING id, drive_name, quota_gb;
|
||||
|
||||
\echo ''
|
||||
\echo '測試查詢 v_employee_full_info 視圖:'
|
||||
SELECT
|
||||
emp_no,
|
||||
username_base,
|
||||
legal_name,
|
||||
sso_username,
|
||||
job_title,
|
||||
job_level,
|
||||
business_unit_name,
|
||||
department_name,
|
||||
drive_name,
|
||||
nas_quota_gb
|
||||
FROM v_employee_full_info
|
||||
WHERE username_base = 'porsche.chen'
|
||||
ORDER BY is_primary DESC;
|
||||
|
||||
ROLLBACK;
|
||||
|
||||
\echo ''
|
||||
\echo '✓ 測試資料已回滾 (不影響資料庫)'
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 8. 測試唯一約束 (一個員工在同一事業部只能有一個身份)
|
||||
-- ============================================================================
|
||||
\echo '8. 測試唯一約束 (一個員工在同一事業部只能有一個身份)...'
|
||||
\echo ''
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
-- 嘗試插入重複身份 (應該失敗)
|
||||
INSERT INTO employees (employee_id, username_base, legal_name, hire_date)
|
||||
VALUES ('TEST001', 'test.user', '測試用戶', CURRENT_DATE);
|
||||
|
||||
INSERT INTO employee_identities (
|
||||
employee_id, username, keycloak_id, business_unit_id,
|
||||
job_title, job_level, email_quota_mb, started_at
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM employees WHERE employee_id = 'TEST001'),
|
||||
'test.user@lab.taipei',
|
||||
'test-uuid-1',
|
||||
(SELECT id FROM business_units WHERE code = 'smart'),
|
||||
'測試職位1',
|
||||
'Junior',
|
||||
1000,
|
||||
CURRENT_DATE
|
||||
);
|
||||
|
||||
INSERT INTO employee_identities (
|
||||
employee_id, username, keycloak_id, business_unit_id,
|
||||
job_title, job_level, email_quota_mb, started_at
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM employees WHERE employee_id = 'TEST001'),
|
||||
'test.user@lab.taipei',
|
||||
'test-uuid-2',
|
||||
(SELECT id FROM business_units WHERE code = 'smart'),
|
||||
'測試職位2',
|
||||
'Junior',
|
||||
1000,
|
||||
CURRENT_DATE
|
||||
);
|
||||
|
||||
RAISE NOTICE '✗ 唯一約束測試失敗: 允許插入重複身份';
|
||||
ROLLBACK;
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RAISE NOTICE '✓ 唯一約束測試成功: 正確阻止重複身份';
|
||||
ROLLBACK;
|
||||
END;
|
||||
END $$;
|
||||
|
||||
\echo ''
|
||||
|
||||
-- ============================================================================
|
||||
-- 9. 統計資訊
|
||||
-- ============================================================================
|
||||
\echo '9. 資料庫統計資訊...'
|
||||
\echo ''
|
||||
|
||||
SELECT
|
||||
'employees' AS table_name,
|
||||
COUNT(*) AS row_count
|
||||
FROM employees
|
||||
UNION ALL
|
||||
SELECT 'business_units', COUNT(*) FROM business_units
|
||||
UNION ALL
|
||||
SELECT 'departments', COUNT(*) FROM departments
|
||||
UNION ALL
|
||||
SELECT 'employee_identities', COUNT(*) FROM employee_identities
|
||||
UNION ALL
|
||||
SELECT 'network_drives', COUNT(*) FROM network_drives
|
||||
UNION ALL
|
||||
SELECT 'audit_logs', COUNT(*) FROM audit_logs
|
||||
ORDER BY table_name;
|
||||
|
||||
\echo ''
|
||||
\echo '========================================='
|
||||
\echo '測試完成!'
|
||||
\echo '========================================='
|
||||
\echo ''
|
||||
Reference in New Issue
Block a user