generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } // ===== Enums ===== enum UserRole { MEMBER ADMIN } enum CardTypeCategory { TIMES DURATION TRIAL } enum MembershipStatus { ACTIVE EXPIRED USED_UP } enum TimeSlotStatus { OPEN FULL CLOSED } enum TimeSlotSource { TEMPLATE MANUAL } enum BookingStatus { PENDING_CONFIRMATION CONFIRMED CANCELLED COMPLETED NO_SHOW } enum OrderStatus { PENDING PAID REFUNDED } enum InviteReferralStatus { REGISTERED TRIAL_PURCHASED QUALIFIED } // ===== Models ===== model User { id String @id @default(uuid()) inviteCode String? @unique @map("invite_code") @db.VarChar(6) openid String @unique unionid String? phone String? nickname String @default("") avatarUrl String? @map("avatar_url") role UserRole @default(MEMBER) adminBookingSubscriptionCount Int @default(0) @map("admin_booking_subscription_count") lastLoginAt DateTime? @map("last_login_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") bodyMetrics BodyMetric[] memberNotes MemberNote[] progressPhotos ProgressPhoto[] lessonSupplements LessonSupplement[] memberships Membership[] bookings Booking[] orders Order[] subscriptionMessageConsents SubscriptionMessageConsent[] sentInviteReferrals InviteReferral[] @relation("InviteReferralInviter") receivedInviteReferral InviteReferral[] @relation("InviteReferralInvitee") inviteRewardGrants InviteRewardGrant[] @relation("InviteRewardGrantInviter") bodyPortraitVisits BodyPortraitVisit[] @relation("PortraitVisitUser") referredPortraitVisits BodyPortraitVisit[] @relation("PortraitReferrer") bodyPortraitAssessments BodyPortraitAssessment[] growthLead GrowthLead? growthEvents GrowthEvent[] professionalAssessments ProfessionalAssessmentSession[] trainingPlans TrainingPlan[] reassessmentTodos ReassessmentTodo[] growthShareCards GrowthShareCard[] @@map("users") } model SubscriptionMessageConsent { id String @id @default(uuid()) userId String @map("user_id") templateId String @map("template_id") scene String totalRequestCount Int @default(0) @map("total_request_count") acceptCount Int @default(0) @map("accept_count") rejectCount Int @default(0) @map("reject_count") banCount Int @default(0) @map("ban_count") filterCount Int @default(0) @map("filter_count") sentCount Int @default(0) @map("sent_count") lastResult String @map("last_result") lastRequestedAt DateTime @default(now()) @map("last_requested_at") lastSentAt DateTime? @map("last_sent_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id]) @@unique([userId, templateId, scene]) @@index([userId]) @@index([scene]) @@map("subscription_message_consents") } model CardType { id String @id @default(uuid()) name String type CardTypeCategory totalTimes Int? @map("total_times") durationDays Int @map("duration_days") price Decimal @db.Decimal(10, 0) originalPrice Decimal? @map("original_price") @db.Decimal(10, 0) description String? coverUrl String? @map("cover_url") isActive Boolean @default(true) @map("is_active") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") memberships Membership[] orders Order[] @@map("card_types") } model Membership { id String @id @default(uuid()) userId String @map("user_id") cardTypeId String @map("card_type_id") remainingTimes Int? @map("remaining_times") totalTimes Int? @map("total_times") startDate DateTime @map("start_date") expireDate DateTime @map("expire_date") status MembershipStatus @default(ACTIVE) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") lessonSupplements LessonSupplement[] user User @relation(fields: [userId], references: [id]) cardType CardType @relation(fields: [cardTypeId], references: [id]) bookings Booking[] orders Order[] inviteRewardGrants InviteRewardGrant[] @@index([userId]) @@index([userId, cardTypeId]) @@index([status]) @@map("memberships") } model WeekTemplate { id String @id @default(uuid()) dayOfWeek Int @map("day_of_week") startTime String @map("start_time") endTime String @map("end_time") capacity Int @default(1) isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") timeSlots TimeSlot[] @@map("week_templates") } model TimeSlot { id String @id @default(uuid()) date DateTime @db.Date startTime String @map("start_time") endTime String @map("end_time") capacity Int @default(1) bookedCount Int @default(0) @map("booked_count") status TimeSlotStatus @default(OPEN) source TimeSlotSource @default(TEMPLATE) templateId String? @map("template_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") template WeekTemplate? @relation(fields: [templateId], references: [id]) bookings Booking[] @@unique([date, startTime, endTime]) @@index([date]) @@index([status]) @@map("time_slots") } model Booking { id String @id @default(uuid()) userId String @map("user_id") timeSlotId String @map("time_slot_id") membershipId String @map("membership_id") membershipTimesDeducted Boolean? @map("membership_times_deducted") status BookingStatus @default(PENDING_CONFIRMATION) cancelledAt DateTime? @map("cancelled_at") confirmedAt DateTime? @map("confirmed_at") completedAt DateTime? @map("completed_at") operatorId String? @map("operator_id") originAssessmentId String? @map("origin_assessment_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id]) timeSlot TimeSlot @relation(fields: [timeSlotId], references: [id]) membership Membership @relation(fields: [membershipId], references: [id]) originAssessment BodyPortraitAssessment? @relation(fields: [originAssessmentId], references: [id]) qualifiedInviteReferrals InviteReferral[] professionalAssessments ProfessionalAssessmentSession[] review BookingReview? memberNotes MemberNote[] reviewReminderDueAt DateTime? @map("review_reminder_due_at") reviewReminderClaimedAt DateTime? @map("review_reminder_claimed_at") reviewReminderSentAt DateTime? @map("review_reminder_sent_at") classReminderDueAt DateTime? @map("class_reminder_due_at") classReminderClaimedAt DateTime? @map("class_reminder_claimed_at") classReminderSentAt DateTime? @map("class_reminder_sent_at") statusHistory BookingStatusHistory[] @@unique([userId, timeSlotId]) @@index([userId]) @@index([status]) @@index([originAssessmentId]) @@map("bookings") } model BookingStatusHistory { id String @id @default(uuid()) bookingId String @map("booking_id") fromStatus String? @map("from_status") toStatus String @map("to_status") operatorId String? @map("operator_id") remark String? createdAt DateTime @default(now()) @map("created_at") booking Booking @relation(fields: [bookingId], references: [id]) @@index([bookingId]) @@map("booking_status_history") } model Order { id String @id @default(uuid()) userId String @map("user_id") cardTypeId String @map("card_type_id") membershipId String? @map("membership_id") orderNo String @unique @map("order_no") inviteInviterId String? @map("invite_inviter_id") purchasedCategory String? @map("purchased_category") amount Decimal @db.Decimal(10, 0) status OrderStatus @default(PENDING) wxTransactionId String? @map("wx_transaction_id") paidAt DateTime? @map("paid_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id]) cardType CardType @relation(fields: [cardTypeId], references: [id]) membership Membership? @relation(fields: [membershipId], references: [id]) inviteReferrals InviteReferral[] @@index([userId]) @@index([status]) @@index([membershipId]) @@map("orders") } model InviteReferral { id String @id @default(uuid()) inviterId String @map("inviter_id") inviteeId String @unique @map("invitee_id") status InviteReferralStatus @default(REGISTERED) trialOrderId String? @unique @map("trial_order_id") qualifiedBookingId String? @unique @map("qualified_booking_id") invitedAt DateTime @default(now()) @map("invited_at") trialPurchasedAt DateTime? @map("trial_purchased_at") qualifiedAt DateTime? @map("qualified_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") inviter User @relation("InviteReferralInviter", fields: [inviterId], references: [id]) invitee User @relation("InviteReferralInvitee", fields: [inviteeId], references: [id]) trialOrder Order? @relation(fields: [trialOrderId], references: [id]) qualifiedBooking Booking? @relation(fields: [qualifiedBookingId], references: [id]) @@unique([inviterId, inviteeId]) @@index([inviterId, status]) @@index([status]) @@map("invite_referrals") } model InviteRewardGrant { id String @id @default(uuid()) inviterId String @map("inviter_id") membershipId String? @map("membership_id") qualifiedReferralCount Int @map("qualified_referral_count") rewardTimes Int @default(1) @map("reward_times") grantedAt DateTime @default(now()) @map("granted_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") inviter User @relation("InviteRewardGrantInviter", fields: [inviterId], references: [id]) membership Membership? @relation(fields: [membershipId], references: [id], onDelete: Restrict) @@index([inviterId, grantedAt]) @@map("invite_reward_grants") } model StudioConfig { id String @id @default(uuid()) name String logo String? bannerUrl String? @map("banner_url") address String @default("") phone String @default("") latitude Decimal? @db.Decimal(10, 7) longitude Decimal? @db.Decimal(10, 7) cancelHoursLimit Int @default(2) @map("cancel_hours_limit") photos Json @default("[]") updatedAt DateTime @updatedAt @map("updated_at") @@map("studio_config") } // Historical totals without fabricated scheduled dates. model LessonSupplement { id String @id @default(uuid()) userId String @map("user_id") requestId String @map("request_id") @db.VarChar(80) quantity Int membershipId String? @map("membership_id") deductedTimes Int @default(0) @map("deducted_times") cardName String? @map("card_name") remark String? @db.VarChar(200) operatorId String @map("operator_id") operatorName String @map("operator_name") createdAt DateTime @default(now()) @map("created_at") revokedAt DateTime? @map("revoked_at") revokedBy String? @map("revoked_by") user User @relation(fields: [userId], references: [id]) membership Membership? @relation(fields: [membershipId], references: [id], onDelete: Restrict) @@unique([userId, requestId]) @@index([userId, revokedAt, createdAt]) @@map("lesson_supplements") } model BookingReview { id String @id @default(uuid()) bookingId String @unique @map("booking_id") rating Int recommendation Int? tags Json comment String @db.VarChar(200) createdAt DateTime @default(now()) @map("created_at") booking Booking @relation(fields: [bookingId], references: [id]) @@index([createdAt]) @@map("booking_reviews") } model BodyMetric { id String @id @default(uuid()) userId String @map("user_id") recordedAt DateTime @db.Date @map("recorded_at") weight Float? bodyFat Float? @map("body_fat") waist Float? hip Float? flexibility Float? remark String @default("") @db.VarChar(200) operatorId String @map("operator_id") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id]) @@index([userId, recordedAt]) @@map("body_metrics") } model MemberNote { id String @id @default(uuid()) userId String @map("user_id") bookingId String? @map("booking_id") content String @db.VarChar(1000) shared Boolean @default(false) operatorId String @map("operator_id") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id]) booking Booking? @relation(fields: [bookingId], references: [id]) @@index([userId, createdAt]) @@map("member_notes") } model ProgressPhoto { id String @id @default(uuid()) userId String @map("user_id") objectKey String @unique @map("object_key") caption String @default("") @db.VarChar(200) recordedAt DateTime @db.Date @map("recorded_at") uploadedAt DateTime? @map("uploaded_at") consentedAt DateTime? @map("consented_at") revokedAt DateTime? @map("revoked_at") operatorId String @map("operator_id") angle String? @db.VarChar(16) sessionId String? @map("session_id") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id]) session ProfessionalAssessmentSession? @relation(fields: [sessionId], references: [id]) @@index([userId, recordedAt]) @@index([sessionId]) @@map("progress_photos") } model BodyPortraitVisit { id String @id @default(uuid()) tokenHash String @unique @map("token_hash") @db.VarChar(64) source String @default("organic") @db.VarChar(32) campaignId String? @map("campaign_id") @db.VarChar(40) referralCode String? @map("referral_code") @db.VarChar(12) referrerUserId String? @map("referrer_user_id") landingPath String @default("") @map("landing_path") @db.VarChar(120) userId String? @map("user_id") firstSeenAt DateTime @default(now()) @map("first_seen_at") lastSeenAt DateTime @default(now()) @map("last_seen_at") createdAt DateTime @default(now()) @map("created_at") user User? @relation("PortraitVisitUser", fields: [userId], references: [id]) referrer User? @relation("PortraitReferrer", fields: [referrerUserId], references: [id]) assessments BodyPortraitAssessment[] events GrowthEvent[] @@index([source, createdAt]) @@index([userId]) @@map("body_portrait_visits") } model BodyPortraitAssessment { id String @id @default(uuid()) visitId String @map("visit_id") userId String? @map("user_id") tokenHash String @unique @map("token_hash") @db.VarChar(64) questionnaireVersion String @map("questionnaire_version") @db.VarChar(32) rulesVersion String @map("rules_version") @db.VarChar(32) status String @default("DRAFT") @db.VarChar(16) answers Json @default("{}") safetyFlagged Boolean @default(false) @map("safety_flagged") report Json? completedAt DateTime? @map("completed_at") claimedAt DateTime? @map("claimed_at") expiresAt DateTime @map("expires_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") visit BodyPortraitVisit @relation(fields: [visitId], references: [id]) user User? @relation(fields: [userId], references: [id]) bookings Booking[] growthLeads GrowthLead[] events GrowthEvent[] professionalAssessments ProfessionalAssessmentSession[] @@index([visitId]) @@index([userId, createdAt]) @@index([status, expiresAt]) @@map("body_portrait_assessments") } model GrowthLead { id String @id @default(uuid()) userId String @unique @map("user_id") latestAssessmentId String? @map("latest_assessment_id") stage String @default("CLAIMED") @db.VarChar(32) source String? @db.VarChar(32) campaignId String? @map("campaign_id") @db.VarChar(40) lastActiveAt DateTime @default(now()) @map("last_active_at") coachNote String @default("") @map("coach_note") @db.VarChar(500) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id]) latestAssessment BodyPortraitAssessment? @relation(fields: [latestAssessmentId], references: [id]) @@index([stage, lastActiveAt]) @@map("growth_leads") } model GrowthEvent { id String @id @default(uuid()) name String @db.VarChar(40) idempotencyKey String @unique @map("idempotency_key") @db.VarChar(120) visitId String? @map("visit_id") assessmentId String? @map("assessment_id") userId String? @map("user_id") properties Json @default("{}") occurredAt DateTime @default(now()) @map("occurred_at") visit BodyPortraitVisit? @relation(fields: [visitId], references: [id]) assessment BodyPortraitAssessment? @relation(fields: [assessmentId], references: [id]) user User? @relation(fields: [userId], references: [id]) @@index([name, occurredAt]) @@index([userId, occurredAt]) @@map("growth_events") } model ProfessionalAssessmentSession { id String @id @default(uuid()) userId String @map("user_id") kind String @db.VarChar(16) protocolVersion String @default("offline-v1") @map("protocol_version") @db.VarChar(32) recordedAt DateTime @db.Date @map("recorded_at") bookingId String? @map("booking_id") originAssessmentId String? @map("origin_assessment_id") observations Json subjectiveTension Float? @map("subjective_tension") coachSummary String @default("") @map("coach_summary") @db.VarChar(1000) trainingFocus String @default("") @map("training_focus") @db.VarChar(500) phaseGoal String @default("") @map("phase_goal") @db.VarChar(500) photoAngle String? @map("photo_angle") @db.VarChar(16) operatorId String @map("operator_id") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id]) booking Booking? @relation(fields: [bookingId], references: [id]) originAssessment BodyPortraitAssessment? @relation(fields: [originAssessmentId], references: [id]) photos ProgressPhoto[] trainingPlans TrainingPlan[] @@index([userId, recordedAt]) @@map("professional_assessment_sessions") } model TrainingPlan { id String @id @default(uuid()) userId String @map("user_id") sessionId String? @map("session_id") title String @db.VarChar(80) status String @default("ACTIVE") @db.VarChar(16) weeks Int @default(12) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id]) session ProfessionalAssessmentSession? @relation(fields: [sessionId], references: [id]) phases TrainingPlanPhase[] todos ReassessmentTodo[] shares GrowthShareCard[] @@index([userId, createdAt]) @@map("training_plans") } model TrainingPlanPhase { id String @id @default(uuid()) planId String @map("plan_id") name String @db.VarChar(40) lessonStart Int @map("lesson_start") lessonEnd Int @map("lesson_end") focus Json summary String @default("") @db.VarChar(300) sortOrder Int @default(0) @map("sort_order") plan TrainingPlan @relation(fields: [planId], references: [id], onDelete: Cascade) @@index([planId]) @@map("training_plan_phases") } model ReassessmentTodo { id String @id @default(uuid()) userId String @map("user_id") planId String @map("plan_id") lessonCheckpoint Int @map("lesson_checkpoint") dueAt DateTime @map("due_at") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id]) plan TrainingPlan @relation(fields: [planId], references: [id], onDelete: Cascade) @@unique([planId, lessonCheckpoint]) @@index([userId, completedAt]) @@map("reassessment_todos") } model GrowthShareCard { id String @id @default(uuid()) userId String @map("user_id") planId String? @map("plan_id") shareCode String @unique @map("share_code") @db.VarChar(12) title String @db.VarChar(80) caption String @db.VarChar(200) completedCount Int @map("completed_count") weeks Int @default(12) rows Json includePhotos Boolean @default(false) @map("include_photos") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id]) plan TrainingPlan? @relation(fields: [planId], references: [id]) @@index([userId, createdAt]) @@map("growth_share_cards") }