feat: 支持会员卡合同在线签署

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-10-28 12:24:22 +08:00
parent c04a9db627
commit ecea65f596
46 changed files with 4313 additions and 232 deletions

View File

@@ -58,6 +58,17 @@ enum InviteReferralStatus {
QUALIFIED
}
enum MembershipContractStatus {
PENDING_SIGN
SIGNED
VOIDED
}
enum MembershipContractSource {
PURCHASE
LEGACY
}
// ===== Models =====
model User {
@@ -95,6 +106,7 @@ model User {
trainingPlans TrainingPlan[]
reassessmentTodos ReassessmentTodo[]
growthShareCards GrowthShareCard[]
membershipContracts MembershipContract[]
@@map("users")
}
@@ -163,6 +175,7 @@ model Membership {
bookings Booking[]
orders Order[]
inviteRewardGrants InviteRewardGrant[]
contracts MembershipContract[]
@@index([userId])
@@index([userId, cardTypeId])
@@ -273,6 +286,7 @@ model Order {
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")
@@ -280,6 +294,7 @@ model Order {
cardType CardType @relation(fields: [cardTypeId], references: [id])
membership Membership? @relation(fields: [membershipId], references: [id])
inviteReferrals InviteReferral[]
contract MembershipContract?
@@index([userId])
@@index([status])
@@ -287,6 +302,84 @@ model Order {
@@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")