280 lines
8.8 KiB
Plaintext
280 lines
8.8 KiB
Plaintext
// 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
|
||
}
|
||
|
||
// ============================================================================
|
||
// CDC 幂等消费追踪
|
||
// ============================================================================
|
||
|
||
// 已处理 CDC 事件表(幂等性)
|
||
// 使用 (sourceTopic, offset) 作为复合唯一键
|
||
// 这是事务性幂等消费的关键:在同一事务中插入此记录 + 执行业务逻辑
|
||
model ProcessedCdcEvent {
|
||
id BigInt @id @default(autoincrement())
|
||
sourceTopic String @map("source_topic") @db.VarChar(200) // CDC topic 名称
|
||
offset BigInt @map("offset") // Kafka offset 作为唯一标识
|
||
|
||
tableName String @map("table_name") @db.VarChar(100) // 表名
|
||
operation String @map("operation") @db.VarChar(10) // c/u/d/r
|
||
processedAt DateTime @default(now()) @map("processed_at")
|
||
|
||
@@unique([sourceTopic, offset])
|
||
@@index([processedAt])
|
||
@@map("processed_cdc_events")
|
||
}
|