// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================================================ // 2.0 用户表 // ============================================================================ model User { id BigInt @id @default(autoincrement()) // 基本信息 phone String @unique passwordHash String @map("password_hash") // 统一关联键 (跨所有服务) // V1: 12位 (D + 6位日期 + 5位序号), 如 D2512110008 // V2: 15位 (D + 6位日期 + 8位序号), 如 D25121100000008 accountSequence String @unique @map("account_sequence") // 状态 status UserStatus @default(ACTIVE) // KYC 实名认证 kycStatus KycStatus @default(PENDING) realName String? @map("real_name") idCardNo String? @map("id_card_no") idCardFront String? @map("id_card_front") // 身份证正面照片路径 idCardBack String? @map("id_card_back") // 身份证背面照片路径 kycSubmittedAt DateTime? @map("kyc_submitted_at") kycVerifiedAt DateTime? @map("kyc_verified_at") kycRejectReason String? @map("kyc_reject_reason") // 安全 loginFailCount Int @default(0) @map("login_fail_count") lockedUntil DateTime? @map("locked_until") lastLoginAt DateTime? @map("last_login_at") lastLoginIp String? @map("last_login_ip") // 时间戳 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 refreshTokens RefreshToken[] loginLogs LoginLog[] smsLogs SmsLog[] @@index([phone]) @@index([accountSequence]) @@index([status]) @@index([kycStatus]) @@map("users") } enum UserStatus { ACTIVE DISABLED DELETED } enum KycStatus { PENDING // 待提交 SUBMITTED // 已提交待审核 VERIFIED // 已认证 REJECTED // 已拒绝 } // ============================================================================ // CDC 同步的 1.0 用户(只读) // ============================================================================ model SyncedLegacyUser { id BigInt @id @default(autoincrement()) // 1.0 用户数据 legacyId BigInt @unique @map("legacy_id") // 1.0 的 user.id accountSequence String @unique @map("account_sequence") phone String? // 系统账户可能没有手机号 passwordHash String? @map("password_hash") // 系统账户可能没有密码 nickname String? // 昵称 (from identity-service) status String legacyCreatedAt DateTime @map("legacy_created_at") // 迁移状态 migratedToV2 Boolean @default(false) @map("migrated_to_v2") migratedAt DateTime? @map("migrated_at") // CDC 元数据 sourceSequenceNum BigInt @map("source_sequence_num") syncedAt DateTime @default(now()) @map("synced_at") @@index([phone]) @@index([accountSequence]) @@index([migratedToV2]) @@map("synced_legacy_users") } // ============================================================================ // 刷新令牌 // ============================================================================ model RefreshToken { id BigInt @id @default(autoincrement()) userId BigInt @map("user_id") token String @unique deviceInfo String? @map("device_info") ipAddress String? @map("ip_address") expiresAt DateTime @map("expires_at") createdAt DateTime @default(now()) @map("created_at") revokedAt DateTime? @map("revoked_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([token]) @@index([expiresAt]) @@map("refresh_tokens") } // ============================================================================ // 短信验证码 // ============================================================================ model SmsVerification { id BigInt @id @default(autoincrement()) phone String code String type SmsVerificationType expiresAt DateTime @map("expires_at") verifiedAt DateTime? @map("verified_at") attempts Int @default(0) createdAt DateTime @default(now()) @map("created_at") @@index([phone, type]) @@index([expiresAt]) @@map("sms_verifications") } enum SmsVerificationType { REGISTER // 注册 LOGIN // 登录 RESET_PASSWORD // 重置密码 CHANGE_PHONE // 更换手机号 } // ============================================================================ // 短信发送日志 // ============================================================================ model SmsLog { id BigInt @id @default(autoincrement()) userId BigInt? @map("user_id") phone String type SmsVerificationType content String? status SmsStatus @default(PENDING) provider String? // 短信服务商 providerId String? @map("provider_id") // 服务商返回的ID errorMsg String? @map("error_msg") createdAt DateTime @default(now()) @map("created_at") user User? @relation(fields: [userId], references: [id]) @@index([phone]) @@index([userId]) @@index([createdAt]) @@map("sms_logs") } enum SmsStatus { PENDING SENT DELIVERED FAILED } // ============================================================================ // 登录日志 // ============================================================================ model LoginLog { id BigInt @id @default(autoincrement()) userId BigInt? @map("user_id") phone String type LoginType success Boolean failReason String? @map("fail_reason") ipAddress String? @map("ip_address") userAgent String? @map("user_agent") deviceInfo String? @map("device_info") createdAt DateTime @default(now()) @map("created_at") user User? @relation(fields: [userId], references: [id]) @@index([userId]) @@index([phone]) @@index([createdAt]) @@map("login_logs") } enum LoginType { PASSWORD // 密码登录 SMS_CODE // 验证码登录 LEGACY_MIGRATE // 1.0 用户迁移登录 } // ============================================================================ // 每日序号计数器(用于生成 accountSequence) // ============================================================================ model DailySequenceCounter { id BigInt @id @default(autoincrement()) dateKey String @unique @map("date_key") // 格式: YYMMDD lastSeq Int @default(0) @map("last_seq") updatedAt DateTime @updatedAt @map("updated_at") @@map("daily_sequence_counters") } // ============================================================================ // 发件箱(事件发布) // ============================================================================ model OutboxEvent { id BigInt @id @default(autoincrement()) aggregateType String @map("aggregate_type") aggregateId String @map("aggregate_id") eventType String @map("event_type") payload Json topic String key String status OutboxStatus @default(PENDING) retryCount Int @default(0) @map("retry_count") maxRetries Int @default(3) @map("max_retries") lastError String? @map("last_error") publishedAt DateTime? @map("published_at") nextRetryAt DateTime? @map("next_retry_at") createdAt DateTime @default(now()) @map("created_at") @@index([status]) @@index([nextRetryAt]) @@map("outbox_events") } enum OutboxStatus { PENDING PUBLISHED FAILED }