Files
mp-pilates/packages/server/prisma/schema.prisma
2026-10-28 12:24:22 +08:00

820 lines
31 KiB
Plaintext

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
SCHEDULED
}
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
}
enum MembershipContractStatus {
PENDING_SIGN
SIGNED
VOIDED
}
enum MembershipContractSource {
PURCHASE
LEGACY
}
// ===== 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")
productOrders ProductOrder[]
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[]
membershipContracts MembershipContract[]
@@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[]
contracts MembershipContract[]
@@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")
contractAcceptedAt DateTime? @map("contract_accepted_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[]
contract MembershipContract?
@@index([userId])
@@index([status])
@@index([membershipId])
@@map("orders")
}
// Single-row party B profile; signed contracts keep their own snapshot.
model ContractParty {
id String @id @default(uuid())
businessName String @default("") @map("business_name") @db.VarChar(100)
brandName String @default("") @map("brand_name") @db.VarChar(60)
creditCode String @default("") @map("credit_code") @db.VarChar(18)
operatorName String @default("") @map("operator_name") @db.VarChar(30)
phone String @default("") @db.VarChar(20)
address String @default("") @db.VarChar(200)
premisesType String @default("LEASED") @map("premises_type") @db.VarChar(10)
leaseEndDate DateTime? @map("lease_end_date") @db.Date
standardLessonPrice Int @default(0) @map("standard_lesson_price")
signatureImage Bytes? @map("signature_image") @db.MediumBlob
signatureSha256 String? @map("signature_sha256") @db.VarChar(64)
signatureUpdatedAt DateTime? @map("signature_updated_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("contract_parties")
}
// terms is frozen at issuance; document, hashes and signatures are frozen at signing.
model MembershipContract {
id String @id @default(uuid())
contractNo String @unique @map("contract_no") @db.VarChar(32)
userId String @map("user_id")
membershipId String @map("membership_id")
orderId String? @unique @map("order_id")
source MembershipContractSource
status MembershipContractStatus @default(PENDING_SIGN)
requireBeforeBooking Boolean @default(false) @map("require_before_booking")
templateVersion String @map("template_version") @db.VarChar(32)
terms Json
partyName String? @map("party_name") @db.VarChar(30)
partyPhone String? @map("party_phone") @db.VarChar(20)
document Json?
contentSha256 String? @map("content_sha256") @db.VarChar(64)
signatureImage Bytes? @map("signature_image") @db.MediumBlob
signatureSha256 String? @map("signature_sha256") @db.VarChar(64)
partyBSignatureImage Bytes? @map("party_b_signature_image") @db.MediumBlob
partyBName String? @map("party_b_name") @db.VarChar(100)
signedAt DateTime? @map("signed_at")
signerIp String? @map("signer_ip") @db.VarChar(64)
signerUserAgent String? @map("signer_user_agent") @db.VarChar(300)
signerDevice String? @map("signer_device") @db.VarChar(200)
voidedAt DateTime? @map("voided_at")
voidedBy String? @map("voided_by")
voidReason String? @map("void_reason") @db.VarChar(200)
createdBy String? @map("created_by")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id])
membership Membership @relation(fields: [membershipId], references: [id])
order Order? @relation(fields: [orderId], references: [id])
events ContractEvent[]
@@index([userId, status])
@@index([membershipId, status])
@@index([status, createdAt])
@@map("membership_contracts")
}
model ContractEvent {
id String @id @default(uuid())
contractId String @map("contract_id")
action String @db.VarChar(20)
actorId String? @map("actor_id")
ip String? @db.VarChar(64)
userAgent String? @map("user_agent") @db.VarChar(300)
detail Json?
createdAt DateTime @default(now()) @map("created_at")
contract MembershipContract @relation(fields: [contractId], references: [id])
@@index([contractId, createdAt])
@@map("contract_events")
}
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("[]")
ownerIntroduction String? @map("owner_introduction") @db.Text
ownerCertifications Json? @map("owner_certifications")
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")
height Float?
weight Float?
bodyFat Float? @map("body_fat")
waist Float?
hip Float?
flexibility Float?
assessment Json?
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)
apparatusNotes Json? @map("apparatus_notes")
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")
}
// Inventory counts sellable units; pending orders reserve units immediately.
enum ProductOrderStatus {
PENDING
PAID
COMPLETED
CANCELLED
}
model Product {
id String @id @default(uuid())
title String @db.VarChar(80)
description String @db.Text
coverUrl String @map("cover_url") @db.VarChar(1000)
price Int
stock Int @default(0)
sold Int @default(0)
isActive Boolean @default(false) @map("is_active")
sortOrder Int @default(0) @map("sort_order")
version Int @default(0)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orders ProductOrder[]
stockChanges ProductStockChange[]
@@index([isActive, sortOrder])
@@map("products")
}
model ProductOrder {
id String @id @default(uuid())
orderNo String @unique @map("order_no") @db.VarChar(32)
requestId String @map("request_id") @db.VarChar(80)
userId String @map("user_id")
productId String @map("product_id")
title String @db.VarChar(80)
coverUrl String @map("cover_url") @db.VarChar(1000)
unitPrice Int @map("unit_price")
quantity Int
amount Int
contactName String @map("contact_name") @db.VarChar(40)
contactPhone String @map("contact_phone") @db.VarChar(20)
status ProductOrderStatus @default(PENDING)
wxTransactionId String? @unique @map("wx_transaction_id")
expiresAt DateTime @map("expires_at")
paidAt DateTime? @map("paid_at")
completedAt DateTime? @map("completed_at")
completedBy String? @map("completed_by")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id])
product Product @relation(fields: [productId], references: [id])
@@unique([userId, requestId])
@@index([status, expiresAt])
@@index([userId, createdAt])
@@index([productId, status])
@@map("product_orders")
}
model ProductStockChange {
id String @id @default(uuid())
productId String @map("product_id")
delta Int
reason String @db.VarChar(200)
operatorId String @map("operator_id")
createdAt DateTime @default(now()) @map("created_at")
product Product @relation(fields: [productId], references: [id])
@@index([productId, createdAt])
@@map("product_stock_changes")
}
// Public teaching stories never reference private ProgressPhoto records.
model TeachingCase {
id String @id @default(cuid())
title String @db.VarChar(80)
displayName String @db.VarChar(30)
lessonCount Int
background String @db.VarChar(500)
achievement String @db.VarChar(1000)
frames Json
sortOrder Int @default(0)
published Boolean @default(false)
consentConfirmed Boolean @default(false)
consentNote String @db.VarChar(500)
consentedAt DateTime?
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([published, sortOrder, createdAt])
}
model TeachingCaseAsset {
id String @id @default(cuid())
objectKey String @unique @db.VarChar(255)
ready Boolean @default(false)
createdAt DateTime @default(now())
}