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:
186
backend/check_architecture.py
Normal file
186
backend/check_architecture.py
Normal 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()
|
||||
Reference in New Issue
Block a user