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:
2026-02-23 20:12:43 +08:00
commit 360533393f
386 changed files with 70353 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
"""
檢查多租戶架構完整性
"""
import psycopg2
conn = psycopg2.connect('postgresql://admin:DC1qaz2wsx@10.1.0.20:5433/hr_portal')
cur = conn.cursor()
print('=' * 80)
print('Multi-Tenant Architecture Analysis')
print('=' * 80)
# 1. 租戶隔離檢查
print('\n[1] Tenant Isolation (tenant_id FK)')
cur.execute("""
SELECT
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND ccu.table_name = 'tenants'
ORDER BY tc.table_name
""")
tenant_fk_tables = []
for row in cur.fetchall():
tenant_fk_tables.append(row[0])
print(f' [OK] {row[0]:35s} -> tenants.id')
print(f'\n Total: {len(tenant_fk_tables)} tables with tenant isolation')
# 2. 通用欄位檢查
print(f'\n[2] Common Fields Check')
required_cols = ['is_active', 'edit_by', 'created_at', 'updated_at']
all_tables = []
cur.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
AND table_name LIKE 'tenant_%'
ORDER BY table_name
""")
all_tables = [row[0] for row in cur.fetchall()]
complete_count = 0
for table in all_tables:
cur.execute(f"""
SELECT column_name
FROM information_schema.columns
WHERE table_name = '{table}'
AND column_name IN ('is_active', 'edit_by', 'created_at', 'updated_at')
""")
existing = [row[0] for row in cur.fetchall()]
missing = [col for col in required_cols if col not in existing]
if not missing:
print(f' [OK] {table}')
complete_count += 1
else:
print(f' [!!] {table:35s} missing: {", ".join(missing)}')
print(f'\n Complete: {complete_count}/{len(all_tables)} tables')
# 3. 複合主鍵檢查
print(f'\n[3] Composite Primary Key')
cur.execute("""
SELECT
tc.table_name,
string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) as pk_columns
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = 'public'
GROUP BY tc.table_name
HAVING COUNT(kcu.column_name) > 1
ORDER BY tc.table_name
""")
composite_pk = cur.fetchall()
if composite_pk:
for row in composite_pk:
print(f' [OK] {row[0]:35s} PRIMARY KEY ({row[1]})')
else:
print(' (No composite primary key tables)')
# 4. 租戶內序號檢查
print(f'\n[4] Tenant-Scoped Sequence (seq_no)')
cur.execute("""
SELECT table_name
FROM information_schema.columns
WHERE column_name = 'seq_no'
AND table_schema = 'public'
ORDER BY table_name
""")
seq_tables = cur.fetchall()
for row in seq_tables:
print(f' [OK] {row[0]}')
print(f'\n Total: {len(seq_tables)} tables with seq_no')
# 5. 觸發器檢查
print(f'\n[5] Automation Triggers')
cur.execute("""
SELECT
trigger_name,
event_object_table
FROM information_schema.triggers
WHERE trigger_schema = 'public'
AND (trigger_name LIKE '%tenant%'
OR trigger_name LIKE '%seq%'
OR trigger_name LIKE '%emp_code%')
ORDER BY event_object_table, trigger_name
""")
triggers = cur.fetchall()
for row in triggers:
print(f' [OK] {row[1]:35s} <- {row[0]}')
print(f'\n Total: {len(triggers)} triggers')
# 6. 個人化服務
print(f'\n[6] Personal Services')
cur.execute('SELECT service_code, service_name FROM personal_services WHERE is_active = true ORDER BY id')
for row in cur.fetchall():
print(f' [OK] {row[0]:10s} - {row[1]}')
# 7. 核心業務表統計
print(f'\n[7] Table Statistics')
cur.execute("""
SELECT
CASE
WHEN table_name LIKE 'tenant_%' THEN 'tenant_* (Core Business)'
WHEN table_name IN ('personal_services', 'system_functions_cache', 'tenants') THEN 'System Support'
WHEN table_name IN ('business_units', 'employee_identities') THEN 'Deprecated'
ELSE 'Other'
END as category,
COUNT(*) as count
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
AND table_name != 'alembic_version'
GROUP BY category
ORDER BY category
""")
for row in cur.fetchall():
print(f' {row[0]:30s} {row[1]:3d} tables')
# 8. 關鍵設計驗證
print(f'\n[8] Key Design Validation')
# 8.1 員工工號自動生成
cur.execute("""
SELECT proname, prosrc
FROM pg_proc
WHERE proname = 'generate_tenant_emp_code'
""")
if cur.fetchone():
print(' [OK] Employee Code Auto-Generation Function Exists')
else:
print(' [!!] Employee Code Auto-Generation Function Missing')
# 8.2 租戶資料
cur.execute('SELECT id, code, name, prefix, is_sysmana FROM tenants WHERE id = 1')
tenant = cur.fetchone()
if tenant:
print(f' [OK] Tenant 1: {tenant[1]} ({tenant[2]}), prefix={tenant[3]}, sysmana={tenant[4]}')
# 8.3 複合主鍵使用情況
cur.execute("""
SELECT COUNT(*) FROM tenant_emp_setting
""")
emp_setting_count = cur.fetchone()[0]
print(f' [OK] tenant_emp_setting (composite PK): {emp_setting_count} records')
print('\n' + '=' * 80)
print('Architecture Check Complete')
print('=' * 80)
conn.close()