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) passwordHash String? @map("password_hash") @db.VarChar(100) // bcrypt 哈希密码 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") } // ============================================ // Outbox 事件表 - 保证事件可靠发送 // 使用 Outbox Pattern 确保领域事件100%送达 // ============================================ model OutboxEvent { id BigInt @id @default(autoincrement()) @map("outbox_id") // 事件信息 eventType String @map("event_type") @db.VarChar(100) topic String @map("topic") @db.VarChar(100) key String @map("key") @db.VarChar(200) payload Json @map("payload") // 聚合根信息 (用于幂等性检查) aggregateId String @map("aggregate_id") @db.VarChar(100) aggregateType String @map("aggregate_type") @db.VarChar(50) // 发布状态: PENDING, SENT, CONFIRMED, FAILED status String @default("PENDING") @map("status") @db.VarChar(20) retryCount Int @default(0) @map("retry_count") maxRetries Int @default(5) @map("max_retries") lastError String? @map("last_error") @db.Text // 时间戳 createdAt DateTime @default(now()) @map("created_at") publishedAt DateTime? @map("published_at") nextRetryAt DateTime? @map("next_retry_at") @@index([status, createdAt]) @@index([status, nextRetryAt]) @@index([aggregateType, aggregateId]) @@index([topic]) @@map("outbox_events") }