// ============================================================= // CYBER STAR · Prisma Schema // 数据库:MySQL 8 · 部署:火山引擎 RDS // ============================================================= generator client { provider = "prisma-client-js" // native:本地开发(macOS / linux-glibc) // linux-musl-openssl-3.0.x:容器运行时(node:22-alpine) binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } datasource db { provider = "mysql" url = env("DATABASE_URL") } // ============================================================= // 艺人 · 候选偶像 // ============================================================= model Artist { id String @id @db.VarChar(8) // 编号 001 ~ 035 no String @unique @db.VarChar(8) // 展示用编号(带前置零) name String @db.VarChar(50) // 中文名 enName String @map("en_name") @db.VarChar(50) // 英文名 slogan String @db.VarChar(120) // 短宣传语 bio String @db.Text // 详细简介 birthday String @db.VarChar(8) // MM-DD height Int @db.SmallInt // cm cv String? @db.VarChar(80) // CV 配音 themeColor String @map("theme_color") @db.VarChar(10) // 应援色 hex portrait String? @db.VarChar(500) // 立绘主图 URL avatar String? @db.VarChar(500) // 圆形头像 URL videoUrl String? @map("video_url") @db.VarChar(500) // 15s 表演视频 videoPoster String? @map("video_poster") @db.VarChar(500) // 视频封面 tags Json @db.Json // string[] 标签数组 status ArtistStatus @default(ACTIVE) /// 缓存字段:当前票数。定期由后台聚合任务更新,避免实时 SUM(votes)。 voteCount Int @default(0) @map("vote_count") /// 缓存字段:当前排名(1 ~ 35)。同上由后台计算。 currentRank Int? @map("current_rank") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") images ArtistImage[] votes Vote[] supports FanSupport[] snapshots RankingSnapshot[] @@map("artists") @@index([voteCount(sort: Desc)]) } enum ArtistStatus { ACTIVE // 正常参赛 WITHDRAWN // 退赛 DISQUALIFIED // 取消资格 } // 艺人多张展示图(定妆/表演/幕后等) model ArtistImage { id BigInt @id @default(autoincrement()) artistId String @map("artist_id") @db.VarChar(8) url String @db.VarChar(500) type String @db.VarChar(20) // 'portrait' | 'performance' | 'backstage' | 'stage' sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") artist Artist @relation(fields: [artistId], references: [id], onDelete: Cascade) @@map("artist_images") @@index([artistId, sortOrder]) } // ============================================================= // 用户 // ============================================================= model User { id BigInt @id @default(autoincrement()) phone String? @unique @db.VarChar(20) // E.164 格式 email String? @unique @db.VarChar(120) openId String? @unique @map("open_id") @db.VarChar(120) // 微信 openid unionId String? @unique @map("union_id") @db.VarChar(120) // 微信 unionid nickname String @db.VarChar(80) avatar String? @db.VarChar(500) loginType LoginType @default(PHONE) @map("login_type") registerIp String? @map("register_ip") @db.VarChar(45) // IPv4/IPv6 deviceFingerprint String? @map("device_fingerprint") @db.VarChar(120) /// 风控等级 · 数值越大风险越高,由风控系统更新 riskLevel Int @default(0) @map("risk_level") @db.TinyInt status UserStatus @default(NORMAL) lastLoginAt DateTime? @map("last_login_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") votes Vote[] quotas DailyQuota[] signIns SignIn[] supports FanSupport[] invitedBy Invitation[] @relation("invitee") invitations Invitation[] @relation("inviter") @@map("users") @@index([phone]) @@index([deviceFingerprint]) } enum LoginType { PHONE // 手机号 + OTP WECHAT // 微信扫码 EMAIL // 邮箱(境外) } enum UserStatus { NORMAL // 正常 WARNED // 警告 BANNED // 封禁 } // ============================================================= // 投票记录(核心热表) // ============================================================= model Vote { id BigInt @id @default(autoincrement()) userId BigInt @map("user_id") artistId String @map("artist_id") @db.VarChar(8) count Int @default(1) // 单次投票数 source VoteSource @default(QUOTA) ip String? @db.VarChar(45) ua String? @db.VarChar(500) // user agent fingerprint String? @db.VarChar(120) // 设备指纹快照 createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) artist Artist @relation(fields: [artistId], references: [id], onDelete: Cascade) @@map("votes") // 关键索引:用户每日单艺人查询、艺人聚合 @@index([userId, artistId, createdAt]) @@index([artistId, createdAt]) @@index([createdAt]) } enum VoteSource { QUOTA // 每日基础票 SIGNIN // 签到额外票 SHARE // 分享得票 INVITE // 邀请奖励 PAID // 付费购票(若开放) } // ============================================================= // 每日票数余额 // ============================================================= model DailyQuota { id BigInt @id @default(autoincrement()) userId BigInt @map("user_id") date DateTime @db.Date totalQuota Int @default(12) @map("total_quota") // 当日总票数(基础 + 奖励) usedQuota Int @default(0) @map("used_quota") // 已用 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("daily_quota") @@unique([userId, date]) } // ============================================================= // 签到记录 // ============================================================= model SignIn { id BigInt @id @default(autoincrement()) userId BigInt @map("user_id") date DateTime @db.Date streak Int @default(1) // 连续签到天数 bonusVotes Int @default(1) @map("bonus_votes") // 奖励票数 createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("sign_ins") @@unique([userId, date]) } // ============================================================= // 应援关系(用户 ❤ 艺人)· 用于"我的应援"区 // ============================================================= model FanSupport { id BigInt @id @default(autoincrement()) userId BigInt @map("user_id") artistId String @map("artist_id") @db.VarChar(8) votedTotal Int @default(0) @map("voted_total") // 累计已投给该艺人的票数 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) artist Artist @relation(fields: [artistId], references: [id], onDelete: Cascade) @@map("fan_supports") @@unique([userId, artistId]) } // ============================================================= // 邀请记录(病毒传播) // ============================================================= model Invitation { id BigInt @id @default(autoincrement()) inviterId BigInt @map("inviter_id") inviteeId BigInt @unique @map("invitee_id") // 被邀请人只能被邀请一次 bonusGiven Boolean @default(false) @map("bonus_given") // 是否已发放奖励 createdAt DateTime @default(now()) @map("created_at") inviter User @relation("inviter", fields: [inviterId], references: [id], onDelete: Cascade) invitee User @relation("invitee", fields: [inviteeId], references: [id], onDelete: Cascade) @@map("invitations") @@index([inviterId]) } // ============================================================= // 排名快照(用于历史趋势 / 大屏展示) // ============================================================= model RankingSnapshot { id BigInt @id @default(autoincrement()) artistId String @map("artist_id") @db.VarChar(8) date DateTime @db.DateTime(0) // 精确到小时的快照 voteCount Int @map("vote_count") rank Int artist Artist @relation(fields: [artistId], references: [id], onDelete: Cascade) @@map("ranking_snapshots") @@unique([artistId, date]) @@index([date]) } // ============================================================= // 活动配置(开关 / 时间 / 规则) // ============================================================= model ActivityConfig { id Int @id @default(1) // 单行配置 startAt DateTime @map("start_at") endAt DateTime @map("end_at") voteEnabled Boolean @default(true) @map("vote_enabled") // 紧急停止开关 dailyQuota Int @default(12) @map("daily_quota") perArtistLimit Int @default(3) @map("per_artist_limit") // 每艺人每日上限 paidVoteEnabled Boolean @default(false) @map("paid_vote_enabled") updatedAt DateTime @updatedAt @map("updated_at") @@map("activity_config") } // ============================================================= // 风控日志(异常投票审计) // ============================================================= model RiskLog { id BigInt @id @default(autoincrement()) userId BigInt? @map("user_id") ip String? @db.VarChar(45) fingerprint String? @db.VarChar(120) rule String @db.VarChar(80) // 命中的风控规则 detail Json? @db.Json action RiskAction @default(WARN) createdAt DateTime @default(now()) @map("created_at") @@map("risk_logs") @@index([userId, createdAt]) @@index([ip, createdAt]) } enum RiskAction { WARN // 仅记录 CAPTCHA // 触发验证码 BLOCK // 拦截本次操作 BAN // 封号 }