308 lines
13 KiB
Plaintext
308 lines
13 KiB
Plaintext
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
model UserAccount {
|
||
userId BigInt @id @default(autoincrement()) @map("user_id")
|
||
accountSequence String @unique @map("account_sequence") @db.VarChar(12) // 格式: D + YYMMDD + 5位序号
|
||
|
||
phoneNumber String? @unique @map("phone_number") @db.VarChar(20)
|
||
nickname String @db.VarChar(100)
|
||
avatarUrl String? @map("avatar_url") @db.Text
|
||
|
||
inviterSequence String? @map("inviter_sequence") @db.VarChar(12) // 推荐人序列号
|
||
referralCode String @unique @map("referral_code") @db.VarChar(10)
|
||
|
||
kycStatus String @default("NOT_VERIFIED") @map("kyc_status") @db.VarChar(20)
|
||
realName String? @map("real_name") @db.VarChar(100)
|
||
idCardNumber String? @map("id_card_number") @db.VarChar(20)
|
||
idCardFrontUrl String? @map("id_card_front_url") @db.VarChar(500)
|
||
idCardBackUrl String? @map("id_card_back_url") @db.VarChar(500)
|
||
kycVerifiedAt DateTime? @map("kyc_verified_at")
|
||
|
||
status String @default("ACTIVE") @db.VarChar(20)
|
||
|
||
registeredAt DateTime @default(now()) @map("registered_at")
|
||
lastLoginAt DateTime? @map("last_login_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
devices UserDevice[]
|
||
walletAddresses WalletAddress[]
|
||
|
||
@@index([phoneNumber], name: "idx_phone")
|
||
@@index([accountSequence], name: "idx_sequence")
|
||
@@index([referralCode], name: "idx_referral_code")
|
||
@@index([inviterSequence], name: "idx_inviter")
|
||
@@index([kycStatus], name: "idx_kyc_status")
|
||
@@index([status], name: "idx_status")
|
||
@@map("user_accounts")
|
||
}
|
||
|
||
model UserDevice {
|
||
id BigInt @id @default(autoincrement())
|
||
userId BigInt @map("user_id")
|
||
deviceId String @map("device_id") @db.VarChar(100)
|
||
deviceName String? @map("device_name") @db.VarChar(100)
|
||
|
||
// Hardware Info - 设备硬件信息 (JSON 存储完整前端传递的设备信息)
|
||
deviceInfo Json? @map("device_info") // 完整的设备信息 JSON
|
||
platform String? @db.VarChar(20) // ios, android, web (冗余字段,便于查询)
|
||
deviceModel String? @map("device_model") @db.VarChar(100) // iPhone 15 Pro, Pixel 8
|
||
osVersion String? @map("os_version") @db.VarChar(50) // iOS 17.2, Android 14
|
||
appVersion String? @map("app_version") @db.VarChar(20) // 1.0.0
|
||
screenWidth Int? @map("screen_width")
|
||
screenHeight Int? @map("screen_height")
|
||
locale String? @db.VarChar(10) // zh-CN, en-US
|
||
timezone String? @db.VarChar(50) // Asia/Shanghai
|
||
|
||
addedAt DateTime @default(now()) @map("added_at")
|
||
lastActiveAt DateTime @default(now()) @map("last_active_at")
|
||
|
||
user UserAccount @relation(fields: [userId], references: [userId], onDelete: Cascade)
|
||
|
||
@@unique([userId, deviceId], name: "uk_user_device")
|
||
@@index([deviceId], name: "idx_device")
|
||
@@index([userId], name: "idx_user")
|
||
@@index([lastActiveAt], name: "idx_last_active")
|
||
@@index([platform], name: "idx_platform")
|
||
@@map("user_devices")
|
||
}
|
||
|
||
model WalletAddress {
|
||
addressId BigInt @id @default(autoincrement()) @map("address_id")
|
||
userId BigInt @map("user_id")
|
||
|
||
chainType String @map("chain_type") @db.VarChar(20)
|
||
address String @db.VarChar(100)
|
||
publicKey String @map("public_key") @db.VarChar(130) // MPC公钥 (压缩/非压缩格式)
|
||
|
||
// MPC 签名验证字段 - 防止地址被恶意篡改
|
||
addressDigest String @map("address_digest") @db.VarChar(66) // SHA256摘要
|
||
mpcSignatureR String @map("mpc_signature_r") @db.VarChar(66) // 签名R分量
|
||
mpcSignatureS String @map("mpc_signature_s") @db.VarChar(66) // 签名S分量
|
||
mpcSignatureV Int @map("mpc_signature_v") // 签名恢复ID
|
||
|
||
status String @default("ACTIVE") @db.VarChar(20)
|
||
|
||
boundAt DateTime @default(now()) @map("bound_at")
|
||
|
||
user UserAccount @relation(fields: [userId], references: [userId], onDelete: Cascade)
|
||
|
||
@@unique([userId, chainType], name: "uk_user_chain")
|
||
@@unique([chainType, address], name: "uk_chain_address")
|
||
@@index([userId], name: "idx_wallet_user")
|
||
@@index([address], name: "idx_address")
|
||
@@index([publicKey], name: "idx_public_key")
|
||
@@map("wallet_addresses")
|
||
}
|
||
|
||
model AccountSequenceGenerator {
|
||
id Int @id @default(autoincrement())
|
||
dateKey String @unique @map("date_key") @db.VarChar(6) // 格式: YYMMDD
|
||
currentSequence Int @default(0) @map("current_sequence") // 当日序号 (0-99999)
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
@@map("account_sequence_generator")
|
||
}
|
||
|
||
model UserEvent {
|
||
eventId BigInt @id @default(autoincrement()) @map("event_id")
|
||
eventType String @map("event_type") @db.VarChar(50)
|
||
|
||
aggregateId String @map("aggregate_id") @db.VarChar(100)
|
||
aggregateType String @map("aggregate_type") @db.VarChar(50)
|
||
|
||
eventData Json @map("event_data")
|
||
|
||
userId BigInt? @map("user_id")
|
||
occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamp(6)
|
||
version Int @default(1)
|
||
|
||
@@index([aggregateType, aggregateId], name: "idx_aggregate")
|
||
@@index([eventType], name: "idx_event_type")
|
||
@@index([userId], name: "idx_event_user")
|
||
@@index([occurredAt], name: "idx_occurred")
|
||
@@map("user_events")
|
||
}
|
||
|
||
model DeviceToken {
|
||
id BigInt @id @default(autoincrement())
|
||
userId BigInt @map("user_id")
|
||
deviceId String @map("device_id") @db.VarChar(100)
|
||
|
||
refreshTokenHash String @unique @map("refresh_token_hash") @db.VarChar(64)
|
||
|
||
expiresAt DateTime @map("expires_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
revokedAt DateTime? @map("revoked_at")
|
||
|
||
@@index([userId, deviceId], name: "idx_user_device_token")
|
||
@@index([expiresAt], name: "idx_expires")
|
||
@@map("device_tokens")
|
||
}
|
||
|
||
model DeadLetterEvent {
|
||
id BigInt @id @default(autoincrement())
|
||
topic String @db.VarChar(100)
|
||
eventId String @map("event_id") @db.VarChar(100)
|
||
eventType String @map("event_type") @db.VarChar(50)
|
||
|
||
aggregateId String @map("aggregate_id") @db.VarChar(100)
|
||
aggregateType String @map("aggregate_type") @db.VarChar(50)
|
||
payload Json?
|
||
|
||
errorMessage String @map("error_message") @db.Text
|
||
errorStack String? @map("error_stack") @db.Text
|
||
retryCount Int @default(0) @map("retry_count")
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
processedAt DateTime? @map("processed_at")
|
||
|
||
@@index([topic], name: "idx_topic")
|
||
@@index([eventType], name: "idx_dead_letter_event_type")
|
||
@@index([createdAt], name: "idx_dead_letter_created")
|
||
@@index([processedAt], name: "idx_processed")
|
||
@@map("dead_letter_events")
|
||
}
|
||
|
||
model SmsCode {
|
||
id BigInt @id @default(autoincrement())
|
||
phoneNumber String @map("phone_number") @db.VarChar(20)
|
||
code String @db.VarChar(10)
|
||
purpose String @db.VarChar(50)
|
||
|
||
expiresAt DateTime @map("expires_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
usedAt DateTime? @map("used_at")
|
||
|
||
@@index([phoneNumber, purpose], name: "idx_phone_purpose")
|
||
@@index([expiresAt], name: "idx_sms_expires")
|
||
@@map("sms_codes")
|
||
}
|
||
|
||
// MPC 密钥分片存储 - 服务端持有的分片
|
||
model MpcKeyShare {
|
||
shareId BigInt @id @default(autoincrement()) @map("share_id")
|
||
userId BigInt @unique @map("user_id") // 一个用户只有一组MPC密钥
|
||
|
||
publicKey String @unique @map("public_key") @db.VarChar(130) // MPC公钥
|
||
partyIndex Int @map("party_index") // 当前分片的参与方索引 (0=服务端)
|
||
threshold Int @default(2) // 阈值 t
|
||
totalParties Int @default(3) @map("total_parties") // 总参与方 n
|
||
|
||
// 加密存储的分片数据 (AES加密)
|
||
encryptedShareData String @map("encrypted_share_data") @db.Text
|
||
|
||
status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, ROTATED, REVOKED
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
rotatedAt DateTime? @map("rotated_at") // 最后一次密钥轮换时间
|
||
|
||
@@index([publicKey], name: "idx_mpc_public_key")
|
||
@@index([status], name: "idx_mpc_status")
|
||
@@map("mpc_key_shares")
|
||
}
|
||
|
||
// MPC 会话记录 - 用于审计和追踪
|
||
model MpcSession {
|
||
sessionId String @id @map("session_id") @db.VarChar(50)
|
||
sessionType String @map("session_type") @db.VarChar(20) // KEYGEN, SIGNING, ROTATION
|
||
|
||
userId BigInt? @map("user_id")
|
||
publicKey String? @map("public_key") @db.VarChar(130)
|
||
|
||
status String @default("PENDING") @db.VarChar(20) // PENDING, IN_PROGRESS, COMPLETED, FAILED
|
||
errorMessage String? @map("error_message") @db.Text
|
||
|
||
// 签名相关 (仅SIGNING类型)
|
||
messageHash String? @map("message_hash") @db.VarChar(66)
|
||
signatureR String? @map("signature_r") @db.VarChar(66)
|
||
signatureS String? @map("signature_s") @db.VarChar(66)
|
||
signatureV Int? @map("signature_v")
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
completedAt DateTime? @map("completed_at")
|
||
|
||
@@index([sessionType], name: "idx_session_type")
|
||
@@index([userId], name: "idx_session_user")
|
||
@@index([status], name: "idx_session_status")
|
||
@@index([createdAt], name: "idx_session_created")
|
||
@@map("mpc_sessions")
|
||
}
|
||
|
||
// 账户恢复助记词 - 与钱包公钥关联,支持挂失和更换
|
||
model RecoveryMnemonic {
|
||
id BigInt @id @default(autoincrement())
|
||
userId BigInt @map("user_id")
|
||
publicKey String @map("public_key") @db.VarChar(130) // 关联的钱包公钥
|
||
|
||
// 助记词存储 (加密)
|
||
encryptedMnemonic String @map("encrypted_mnemonic") @db.Text // AES加密的助记词
|
||
mnemonicHash String @map("mnemonic_hash") @db.VarChar(64) // SHA256哈希(用于验证)
|
||
|
||
// 状态管理
|
||
status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, REPLACED
|
||
isBackedUp Boolean @default(false) @map("is_backed_up") // 用户是否已备份
|
||
|
||
// 挂失/更换相关
|
||
revokedAt DateTime? @map("revoked_at")
|
||
revokedReason String? @map("revoked_reason") @db.VarChar(200)
|
||
replacedById BigInt? @map("replaced_by_id") // 被哪个新助记词替代
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
@@unique([userId, status], name: "uk_user_active_mnemonic") // 一个用户只有一个ACTIVE助记词
|
||
@@index([userId], name: "idx_recovery_user")
|
||
@@index([publicKey], name: "idx_recovery_public_key")
|
||
@@index([status], name: "idx_recovery_status")
|
||
@@map("recovery_mnemonics")
|
||
}
|
||
|
||
// TOTP 二次验证 - 用于敏感操作 (提现、转账等)
|
||
model UserTotp {
|
||
id BigInt @id @default(autoincrement())
|
||
userId BigInt @unique @map("user_id")
|
||
|
||
// TOTP 密钥 (AES加密存储)
|
||
encryptedSecret String @map("encrypted_secret") @db.VarChar(100)
|
||
|
||
// 状态
|
||
isEnabled Boolean @default(false) @map("is_enabled") // 是否已启用
|
||
isVerified Boolean @default(false) @map("is_verified") // 用户是否已验证过一次
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
enabledAt DateTime? @map("enabled_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
@@index([userId], name: "idx_totp_user")
|
||
@@map("user_totp")
|
||
}
|
||
|
||
// 推荐链接 - 用于追踪不同渠道的邀请
|
||
model ReferralLink {
|
||
linkId BigInt @id @default(autoincrement()) @map("link_id")
|
||
userId BigInt @map("user_id")
|
||
referralCode String @map("referral_code") @db.VarChar(10)
|
||
|
||
shortCode String @unique @map("short_code") @db.VarChar(10) // 短链码
|
||
channel String? @db.VarChar(50) // 渠道: wechat, telegram, twitter, etc.
|
||
campaignId String? @map("campaign_id") @db.VarChar(50) // 活动ID
|
||
|
||
clickCount Int @default(0) @map("click_count") // 点击次数
|
||
registerCount Int @default(0) @map("register_count") // 注册转化数
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
expiresAt DateTime? @map("expires_at") // 过期时间 (可选)
|
||
|
||
@@index([userId], name: "idx_referral_link_user")
|
||
@@index([referralCode], name: "idx_referral_link_code")
|
||
@@index([channel], name: "idx_referral_link_channel")
|
||
@@index([createdAt], name: "idx_referral_link_created")
|
||
@@map("referral_links")
|
||
}
|