✨ Características: - 45 ejercicios universitarios (Basic → Advanced) - Renderizado LaTeX profesional - IA generativa (Z.ai/DashScope) - Docker 9 servicios - Tests 123/123 pasando - Seguridad enterprise (JWT, XSS, Rate limiting) 🐳 Infraestructura: - Next.js 14 + Node.js 20 - PostgreSQL 15 + Redis 7 - Docker Compose completo - Nginx + SSL ready 📚 Documentación: - 5 informes técnicos completos - README profesional - Scripts de deployment automatizados Estado: Producción lista ✅
440 lines
12 KiB
Plaintext
440 lines
12 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
username String @unique
|
|
passwordHash String
|
|
telegramChatId String? @unique @map("telegram_chat_id")
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
lastLoginAt DateTime?
|
|
role UserRole @default(STUDENT)
|
|
timezone String @default("UTC") @map("timezone")
|
|
exerciseAttempts ExerciseAttempt[]
|
|
notifications Notification[]
|
|
passwordResetTokens PasswordResetToken[]
|
|
progress Progress[]
|
|
rankings Ranking[]
|
|
refreshTokens RefreshToken[]
|
|
userAchievements UserAchievement[]
|
|
|
|
@@index([email])
|
|
@@index([username])
|
|
@@index([isActive])
|
|
@@index([role])
|
|
@@map("users")
|
|
}
|
|
|
|
model PasswordResetToken {
|
|
id String @id @default(uuid())
|
|
token String @unique
|
|
userId String
|
|
expiresAt DateTime
|
|
used Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([token])
|
|
@@index([userId])
|
|
@@index([expiresAt])
|
|
@@map("password_reset_tokens")
|
|
}
|
|
|
|
model RefreshToken {
|
|
id String @id @default(uuid())
|
|
token String @unique
|
|
userId String
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
revoked Boolean @default(false)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([token])
|
|
@@index([expiresAt])
|
|
@@index([revoked])
|
|
@@map("refresh_tokens")
|
|
}
|
|
|
|
model ExerciseAttempt {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
exerciseId String
|
|
userAnswer String
|
|
status AttemptStatus
|
|
pointsEarned Int @default(0)
|
|
timeSpentSeconds Int
|
|
hintsUsed Int @default(0)
|
|
feedback String?
|
|
createdAt DateTime @default(now())
|
|
attemptNumber Int
|
|
isPerfect Boolean @default(false)
|
|
skipped Boolean @default(false)
|
|
earnedAt DateTime @default(now())
|
|
exercises Exercise @relation(fields: [exerciseId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, exerciseId, attemptNumber])
|
|
@@index([userId])
|
|
@@index([exerciseId])
|
|
@@index([createdAt])
|
|
@@index([earnedAt])
|
|
@@index([status])
|
|
@@index([userId, exerciseId])
|
|
@@map("exercise_attempts")
|
|
}
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
type NotificationType
|
|
title String
|
|
message String
|
|
user_id String
|
|
status NotificationStatus @default(PENDING)
|
|
priority Int @default(0)
|
|
metadata Json?
|
|
attempts Int @default(0)
|
|
lastAttemptAt DateTime?
|
|
sentAt DateTime?
|
|
errorMessage String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
users User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([createdAt])
|
|
@@index([priority])
|
|
@@index([status])
|
|
@@index([type])
|
|
@@index([user_id])
|
|
@@map("notifications")
|
|
}
|
|
|
|
model Progress {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
moduleId String
|
|
exercisesCompleted Int @default(0)
|
|
totalExercises Int @default(0)
|
|
points Int @default(0)
|
|
percentage Float @default(0)
|
|
isStarted Boolean @default(false)
|
|
isCompleted Boolean @default(false)
|
|
startedAt DateTime?
|
|
completedAt DateTime?
|
|
lastAccessedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
averageScore Float?
|
|
totalTimeSpent Int @default(0)
|
|
perfectExercises Int @default(0)
|
|
attemptsCount Int @default(0)
|
|
modules modules @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, moduleId])
|
|
@@index([userId])
|
|
@@index([moduleId])
|
|
@@index([isCompleted])
|
|
@@index([points])
|
|
@@map("progress")
|
|
}
|
|
|
|
model Ranking {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
moduleId String?
|
|
position Int
|
|
points Int @default(0)
|
|
exercisesCompleted Int @default(0)
|
|
streak Int @default(0)
|
|
lastUpdated DateTime @default(now())
|
|
perfectExercises Int @default(0)
|
|
averageScore Float?
|
|
totalAttempts Int @default(0)
|
|
achievementsUnlocked Int @default(0)
|
|
longestStreak Int @default(0)
|
|
modules modules? @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, moduleId])
|
|
@@index([points])
|
|
@@index([moduleId])
|
|
@@index([position])
|
|
@@index([streak])
|
|
@@map("rankings")
|
|
}
|
|
|
|
model Achievement {
|
|
id String @id @default(cuid())
|
|
code String @unique
|
|
name String
|
|
description String
|
|
category AchievementCategory
|
|
rarity AchievementRarity
|
|
icon String
|
|
requirementType RequirementType
|
|
requirementValue Int
|
|
points Int @default(0)
|
|
metadata Json?
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
userAchievements UserAchievement[]
|
|
|
|
@@index([category])
|
|
@@index([code])
|
|
@@index([isActive])
|
|
@@index([rarity])
|
|
@@map("achievements")
|
|
}
|
|
|
|
model UserAchievement {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
achievementId String
|
|
progress Int @default(0)
|
|
unlockedAt DateTime?
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
achievement Achievement @relation(fields: [achievementId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, achievementId])
|
|
@@index([userId])
|
|
@@index([achievementId])
|
|
@@index([unlockedAt])
|
|
@@map("user_achievements")
|
|
}
|
|
|
|
model Exercise {
|
|
id String @id @default(cuid())
|
|
moduleId String
|
|
topicId String?
|
|
type ExerciseType
|
|
difficulty ExerciseDifficulty
|
|
order Int
|
|
statement String
|
|
correctAnswer String
|
|
solutionSteps Json?
|
|
formulas Json?
|
|
hints Json?
|
|
isAIGenerated Boolean @default(false)
|
|
isPublished Boolean @default(false)
|
|
points Int @default(10)
|
|
timeLimitSeconds Int?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
multipleChoiceOptions Json?
|
|
proofRequirements Json?
|
|
calculationSteps Json?
|
|
exercise_attempts ExerciseAttempt[]
|
|
modules modules @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
|
topics topics? @relation(fields: [topicId], references: [id])
|
|
|
|
@@unique([moduleId, order])
|
|
@@index([difficulty])
|
|
@@index([type])
|
|
@@index([isAIGenerated])
|
|
@@index([isPublished])
|
|
@@index([moduleId])
|
|
@@index([topicId])
|
|
@@map("exercises")
|
|
}
|
|
|
|
model SystemConfig {
|
|
id String @id @default(cuid())
|
|
key String @unique
|
|
value String
|
|
description String?
|
|
category String?
|
|
isPublic Boolean @default(false)
|
|
isEncrypted Boolean @default(false)
|
|
dataType String @default("string")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
createdBy String?
|
|
updatedBy String?
|
|
changeHistory Json?
|
|
|
|
@@index([category])
|
|
@@index([isPublic])
|
|
@@map("system_config")
|
|
}
|
|
|
|
model modules {
|
|
id String @id
|
|
name String
|
|
description String
|
|
type ModuleType
|
|
order Int
|
|
isPublished Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
introduction String?
|
|
examples Json?
|
|
exercisesData Json?
|
|
answers Json?
|
|
estimatedHours Int? @default(0)
|
|
difficultyLevel ExerciseDifficulty @default(INTERMEDIATE)
|
|
totalExercises Int @default(0)
|
|
exercises Exercise[]
|
|
progress Progress[]
|
|
rankings Ranking[]
|
|
topics topics[]
|
|
|
|
@@unique([type, order])
|
|
@@index([isPublished])
|
|
@@index([order])
|
|
@@index([type])
|
|
}
|
|
|
|
model processed_pdfs {
|
|
id String @id
|
|
file_name String @unique
|
|
original_path String
|
|
type PdfType
|
|
topicType TopicType?
|
|
is_processed Boolean @default(false)
|
|
processing_started_at DateTime?
|
|
processing_completed_at DateTime?
|
|
errorMessage String?
|
|
extractedText Json?
|
|
exercisesDetected Json?
|
|
formulasExtracted Json?
|
|
metadata Json?
|
|
totalPages Int?
|
|
processingVersion String?
|
|
checksum String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([file_name])
|
|
@@index([is_processed])
|
|
@@index([topicType])
|
|
@@index([type])
|
|
}
|
|
|
|
model topics {
|
|
id String @id
|
|
moduleId String
|
|
name String
|
|
type TopicType
|
|
order Int
|
|
description String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
theoryContent Json?
|
|
formulas Json?
|
|
keyPoints Json?
|
|
commonMistakes Json?
|
|
exercises Exercise[]
|
|
modules modules @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([moduleId, order])
|
|
@@index([moduleId])
|
|
@@index([type])
|
|
}
|
|
|
|
enum UserRole {
|
|
STUDENT
|
|
TEACHER
|
|
ADMIN
|
|
}
|
|
|
|
enum AchievementCategory {
|
|
EXERCISES
|
|
MODULES
|
|
STREAKS
|
|
RANKING
|
|
SPECIAL
|
|
}
|
|
|
|
enum AchievementRarity {
|
|
COMMON
|
|
RARE
|
|
EPIC
|
|
LEGENDARY
|
|
}
|
|
|
|
enum AttemptStatus {
|
|
CORRECT
|
|
INCORRECT
|
|
PARTIAL
|
|
PENDING
|
|
}
|
|
|
|
enum ExerciseDifficulty {
|
|
BASIC
|
|
INTERMEDIATE
|
|
ADVANCED
|
|
EXPERT
|
|
}
|
|
|
|
enum ExerciseType {
|
|
MULTIPLE_CHOICE
|
|
OPEN_RESPONSE
|
|
CALCULATION
|
|
PROOF
|
|
TRUE_FALSE
|
|
}
|
|
|
|
enum ModuleType {
|
|
FUNDAMENTOS
|
|
SISTEMAS
|
|
APLICACIONES
|
|
}
|
|
|
|
enum NotificationStatus {
|
|
PENDING
|
|
SENT
|
|
FAILED
|
|
}
|
|
|
|
enum NotificationType {
|
|
NEW_USER
|
|
EXERCISE_COMPLETED
|
|
MODULE_COMPLETED
|
|
ACHIEVEMENT_UNLOCKED
|
|
SYSTEM_ERROR
|
|
DAILY_SUMMARY
|
|
RANKING_CHANGED
|
|
}
|
|
|
|
enum PdfType {
|
|
TEXTBOOK
|
|
PRACTICE
|
|
PRACTICE_ANSWERS
|
|
EXAM
|
|
ADDITIONAL_MATERIAL
|
|
}
|
|
|
|
enum RequirementType {
|
|
EXERCISES_COMPLETED
|
|
MODULES_COMPLETED
|
|
PERFECT_SCORES
|
|
STREAK_DAYS
|
|
RANKING_POSITION
|
|
EXERCISES_WITHOUT_HINTS
|
|
EARLY_BIRD
|
|
NIGHT_OWL
|
|
PERFECT_MODULE
|
|
}
|
|
|
|
enum TopicType {
|
|
VECTORES
|
|
MATRICES
|
|
SISTEMAS
|
|
ESPACIOS_VECTORIALES
|
|
PROGRAMACION_LINEAL
|
|
}
|