跳到主要内容

Technical Architecture

This document provides detailed technical architecture, database schemas, APIs, and implementation details for all system modules.


System Architecture Overview

Three-Tier Application Architecture

┌─────────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
├─────────────────────────────────────────────────────────────────┤
│ Hospital Portal (portal/) │ Console (console/) │
│ - React 18 + Vite │ - Next.js 15 │
│ - Ant Design v5 │ - Ant Design v5 │
│ - TanStack Router/Query │ - TanStack Query │
│ - Firebase Hosting │ - Firebase Hosting │
├─────────────────────────────────────────────────────────────────┤
│ Landing Website (landing/) │ Queue Display (queue/) │
│ - Next.js 15 │ - Next.js 15 │
│ - Tailwind CSS + shadcn/ui │ - Real-time SSE │
│ - Server Components │ - TV-optimized UI │
└─────────────────────────────────────────────────────────────────┘
↓ HTTPS/REST API
┌─────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
├─────────────────────────────────────────────────────────────────┤
│ NestJS Modular Monorepo (server/) │
│ ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ auth │ hms │ crm │ payment │ queue │ │
│ ├──────────┴──────────┴──────────┴──────────┴──────────┤ │
│ │ report │ │
│ └───────────────────────────────────────────────────────┘ │
│ Shared Libraries: prisma/ | common/ | redis/ │
│ Deployment: DigitalOcean App Platform │
└─────────────────────────────────────────────────────────────────┘
↓ Prisma ORM
┌─────────────────────────────────────────────────────────────────┐
│ DATA LAYER │
├─────────────────────────────────────────────────────────────────┤
│ PostgreSQL (Primary DB) │ Redis (Cache/Queue) │
│ - Multi-tenant data │ - Session management │
│ - Prisma migrations │ - Bull queue jobs │
│ - Full-text search │ - Real-time updates │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ IDENTITY LAYER │
├─────────────────────────────────────────────────────────────────┤
│ Keycloak (Identity Provider) │
│ - OAuth2 / OpenID Connect │ - User authentication │
│ - Multi-tenant realms │ - Role-based access │
│ - 2FA (Email OTP) │ - Session management │
│ - Impersonation │ - Audit logging │
└─────────────────────────────────────────────────────────────────┘

Backend Architecture (NestJS Monorepo)

Monorepo Structure

server/
├── apps/ # Modular applications
│ ├── auth/ # Authentication service (Keycloak integration)
│ ├── hms/ # Hospital Management System
│ ├── crm/ # Customer Relationship Management
│ ├── payment/ # Financial management
│ ├── queue/ # Queue & appointment system
│ ├── report/ # Report & Analytics Service
│ └── console/ # Console API (Platform Master)

├── libs/ # Shared libraries
│ ├── prisma/ # Database ORM
│ ├── common/ # Shared utilities
│ └── redis/ # Redis integration

├── prisma/
│ ├── schema.prisma # Database schema
│ ├── migrations/ # Migration history
│ └── seed.ts # Database seeding

└── nest-cli.json # NestJS monorepo config

Service Responsibilities

ServicePurposeKey Modules
authAuthentication & Keycloak integrationKeycloak strategy, guards, 2FA
hmsCore hospital managementPatient, Employee, Lab, Pharmacy
crmCustomer relationshipFamily membership, VIP, Visitors
paymentFinancial operationsInvoice, Receipt, Reconciliation
queueSchedulingAppointments, Queue management
reportAnalyticsFinancial, Clinical, Operational reports
consolePlatform adminHospital CRUD, Role templates

API Design

RESTful API Structure

/api/{service}/{resource}/{action}

Examples:
GET /api/hms/patients # List patients
POST /api/hms/patients # Create patient
GET /api/hms/patients/:id # Get patient
PUT /api/hms/patients/:id # Update patient
DELETE /api/hms/patients/:id # Delete patient

GET /api/crm/families # List families
POST /api/crm/families/:id/members # Add family member

GET /api/payment/invoices # List invoices
POST /api/payment/invoices/:id/pay # Process payment

API Response Format

// Success Response
{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"limit": 20,
"total": 100
}
}

// Error Response
{
"success": false,
"error": {
"code": "PATIENT_NOT_FOUND",
"message": "Patient with ID xxx not found",
"details": { ... }
}
}

Database Design

Multi-Tenancy Model

All tables include hospitalId for tenant isolation:

model Patient {
id String @id @default(uuid())
hospitalId String @map("hospital_id")
hospital Hospital @relation(fields: [hospitalId], references: [id])

firstName String @map("first_name")
lastName String @map("last_name")
phone String
email String?
dateOfBirth DateTime @map("date_of_birth")

createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@map("patients")
@@index([hospitalId])
@@index([phone])
}

Core Entities

EntityDescriptionKey Fields
HospitalTenant organizationid, name, prefix, theme
UserStaff accountsid, hospitalId, email, role
PatientPatient recordsid, hospitalId, name, phone, dob
FamilyFamily membershipid, hospitalId, primaryContactId
InvoiceBilling recordsid, hospitalId, patientId, total
AppointmentScheduled visitsid, hospitalId, patientId, doctorId

Keycloak Authentication

Multi-Tenant Realm Configuration

Each hospital has its own Keycloak realm:

Keycloak
├── master realm (Keycloak admin)
├── ihospita realm (Console users)
├── samaki realm (Hospital A users)
├── royal realm (Hospital B users)
└── ... (one realm per hospital)

Authentication Flow

JWT Token Structure

{
"exp": 1703862400,
"iat": 1703833600,
"iss": "https://auth.ihospita.com/realms/samaki",
"sub": "user-uuid",
"email": "doctor@samaki.com",
"realm_access": {
"roles": ["DOCTOR"]
},
"hospital_id": "hospital-uuid",
"hospital_prefix": "samaki"
}

Real-Time Features

Queue Updates (SSE/WebSocket)

// Server: Queue Service
@Sse('queue/:clinicId/stream')
queueStream(@Param('clinicId') clinicId: string) {
return this.queueService.getQueueStream(clinicId);
}

// Client: Queue Display
const eventSource = new EventSource('/api/queue/clinic-1/stream');
eventSource.onmessage = (event) => {
const queue = JSON.parse(event.data);
updateQueueDisplay(queue);
};

Redis Pub/Sub

// Publisher: When queue changes
await this.redis.publish(`queue:${clinicId}`, JSON.stringify(queue));

// Subscriber: Queue Service
this.redis.subscribe(`queue:${clinicId}`, (message) => {
this.broadcastToClients(clinicId, message);
});

File Storage

S3/Spaces Organization

ihospita-files/
├── samaki/ # Hospital prefix folder
│ ├── branding/ # Logo, favicon
│ ├── patients/ # Patient photos, documents
│ │ └── P-001/
│ │ ├── profile.jpg
│ │ └── id-card.pdf
│ ├── lab-reports/ # Lab result PDFs
│ ├── prescriptions/ # Prescription PDFs
│ └── invoices/ # Invoice PDFs
├── royal/ # Another hospital
└── console/ # Console admin files

Security Implementation

Request Flow with Security

Request → Cloudflare (WAF) → Kong → Service → Database
↓ ↓
DDoS Protection JWT Validation
SSL Termination Rate Limiting
IP Filtering RBAC Check

Guard Implementation

@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
const { user } = context.switchToHttp().getRequest();

return requiredRoles.some((role) => user.roles.includes(role));
}
}

// Usage
@Post('employees')
@Roles('ADMIN', 'OWNER')
async createEmployee(@Body() dto: CreateEmployeeDto) {
// Only ADMIN and OWNER can create employees
}

Error Handling

Global Exception Filter

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();

const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;

const message = exception instanceof HttpException
? exception.message
: 'Internal server error';

response.status(status).json({
success: false,
error: {
code: this.getErrorCode(exception),
message,
timestamp: new Date().toISOString(),
},
});
}
}

Testing Strategy

Test Layers

LayerToolsCoverage Target
Unit TestsJest80%
Integration TestsJest + SupertestCritical paths
E2E TestsCypressUser flows

Example Test

describe('PatientService', () => {
let service: PatientService;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [PatientService, PrismaService],
}).compile();

service = module.get(PatientService);
});

it('should create patient with family', async () => {
const result = await service.create(hospitalId, {
firstName: 'John',
lastName: 'Doe',
phone: '+85512345678',
});

expect(result.patient).toBeDefined();
expect(result.family).toBeDefined();
});
});