Версия цифровой рецепции с резализованным механизмом отслеживания трека пациента по зонам
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

271 lines
8.6 KiB

generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [vector, uuidOssp(map: "uuid-ossp")]
}
// =========================================================================
// AUTH / RBAC
// =========================================================================
enum Role {
MANAGER
SENIOR_ADMIN
SECURITY
SYSADMIN
}
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique
fullName String @map("full_name")
passwordHash String @map("password_hash")
role Role
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
refreshTokens RefreshToken[]
consentActions PatientConsent[]
biometryAccessLog BiometryAccessLog[]
@@map("users")
}
model RefreshToken {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
tokenHash String @unique @map("token_hash")
expiresAt DateTime @map("expires_at")
revokedAt DateTime? @map("revoked_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("refresh_tokens")
}
// =========================================================================
// DOMAIN: PATIENT / CONSENT
// =========================================================================
model Patient {
id String @id @default(uuid()) @db.Uuid
polimedPatientId String? @unique @map("polimed_patient_id")
fullName String? @map("full_name")
consentReceivedAt DateTime? @map("consent_received_at")
consentRevokedAt DateTime? @map("consent_revoked_at")
pendingDeletionAt DateTime? @map("pending_deletion_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
consents PatientConsent[]
tracks Track[]
visits Visit[]
consentRevocationJobs ConsentRevocationJob[]
@@map("patients")
}
enum ConsentAction {
GRANTED
REVOKED
}
model PatientConsent {
id String @id @default(uuid()) @db.Uuid
patientId String @map("patient_id") @db.Uuid
action ConsentAction
paperRef String? @map("paper_ref")
actorUserId String @map("actor_user_id") @db.Uuid
occurredAt DateTime @default(now()) @map("occurred_at")
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
actor User @relation(fields: [actorUserId], references: [id])
@@index([patientId])
@@map("patient_consents")
}
// =========================================================================
// CAMERAS / ZONES
// =========================================================================
enum ZoneCode {
A
B
C
D
E
}
model Zone {
id String @id @default(uuid()) @db.Uuid
code ZoneCode @unique
name String
cameras Camera[]
trackEvents TrackEvent[]
@@map("zones")
}
model Camera {
id String @id @default(uuid()) @db.Uuid
name String @unique
rtspUrl String? @map("rtsp_url")
zoneId String @map("zone_id") @db.Uuid
zone Zone @relation(fields: [zoneId], references: [id])
trackEvents TrackEvent[]
faceEmbeddings FaceEmbedding[]
@@map("cameras")
}
// =========================================================================
// TRACKS / EVENTS / EMBEDDINGS
// =========================================================================
enum TrackStatus {
UNMATCHED
MATCHED
ANONYMIZED
}
model Track {
id String @id @default(uuid()) @db.Uuid
patientId String? @map("patient_id") @db.Uuid
status TrackStatus @default(UNMATCHED)
firstSeenAt DateTime @map("first_seen_at")
lastSeenAt DateTime @map("last_seen_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
patient Patient? @relation(fields: [patientId], references: [id])
events TrackEvent[]
faceEmbeddings FaceEmbedding[]
@@index([status, firstSeenAt])
@@index([patientId])
@@map("tracks")
}
enum TrackEventType {
arrived
waiting
service_started
service_ended
left_without_service
}
model TrackEvent {
id String @id @default(uuid()) @db.Uuid
trackId String @map("track_id") @db.Uuid
type TrackEventType
cameraId String @map("camera_id") @db.Uuid
zoneId String @map("zone_id") @db.Uuid
occurredAt DateTime @map("occurred_at")
evidenceKey String? @map("evidence_key")
// [x1, y1, x2, y2] нормализовано 0..1 — bbox распознанного лица на кадре.
faceBbox Json? @map("face_bbox") @db.JsonB
createdAt DateTime @default(now()) @map("created_at")
track Track @relation(fields: [trackId], references: [id], onDelete: Cascade)
camera Camera @relation(fields: [cameraId], references: [id])
zone Zone @relation(fields: [zoneId], references: [id])
@@index([trackId, occurredAt])
@@map("track_events")
}
// 512-d face embedding (InsightFace buffalo_l).
// face-service пишет и читает напрямую через psycopg2 + pgvector.
model FaceEmbedding {
id String @id @default(uuid()) @db.Uuid
embedding Unsupported("vector(512)")
patientId String? @map("patient_id") @db.Uuid
trackId String? @map("track_id") @db.Uuid
cameraId String @map("camera_id") @db.Uuid
quality Float @default(0)
capturedAt DateTime @map("captured_at")
createdAt DateTime @default(now()) @map("created_at")
track Track? @relation(fields: [trackId], references: [id], onDelete: SetNull)
camera Camera @relation(fields: [cameraId], references: [id])
@@index([trackId])
@@index([patientId])
@@index([capturedAt])
@@map("face_embeddings")
}
// =========================================================================
// VISITS
// =========================================================================
model Visit {
id String @id @default(uuid()) @db.Uuid
patientId String @map("patient_id") @db.Uuid
polimedAppointmentId String? @map("polimed_appointment_id")
arrivedAt DateTime @map("arrived_at")
serviceStartedAt DateTime? @map("service_started_at")
serviceEndedAt DateTime? @map("service_ended_at")
leftWithoutService Boolean @default(false) @map("left_without_service")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
@@index([patientId, arrivedAt])
@@map("visits")
}
// =========================================================================
// AUDIT
// =========================================================================
model BiometryAccessLog {
id String @id @default(uuid()) @db.Uuid
actorUserId String? @map("actor_user_id") @db.Uuid
subjectPatientId String? @map("subject_patient_id") @db.Uuid
action String
requestPath String? @map("request_path")
occurredAt DateTime @default(now()) @map("occurred_at")
actor User? @relation(fields: [actorUserId], references: [id])
@@index([occurredAt])
@@index([subjectPatientId])
@@map("biometry_access_log")
}
// =========================================================================
// CONSENT REVOCATION JOBS (24h delete)
// =========================================================================
enum ConsentRevocationStatus {
PENDING
DONE
}
model ConsentRevocationJob {
id String @id @default(uuid()) @db.Uuid
patientId String @map("patient_id") @db.Uuid
revokedAt DateTime @map("revoked_at")
scheduledFor DateTime @map("scheduled_for")
status ConsentRevocationStatus @default(PENDING)
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
@@index([status, scheduledFor])
@@map("consent_revocation_jobs")
}