UI-UX/prisma/schema.prisma
zyc 7506372abd
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m45s
fix(ci): hoisted node_modules + alpine binary target for Prisma in Docker
Root cause (from build log):
1. Prisma 6 generates client into @prisma/client package dir (not .prisma/client)
2. pnpm default isolated linker puts everything in .pnpm/ store with symlinks
   at top-level — Docker COPY of @prisma followed broken/incomplete symlinks
3. node:22-alpine needs linux-musl-openssl-3.0.x engine binary

Fixes:
- .npmrc: node-linker=hoisted → flat node_modules, COPY behaves like npm
- schema.prisma: add linux-musl-openssl-3.0.x to binaryTargets
- Dockerfile: drop dead .prisma/client checks, copy only @prisma (where
  Prisma 6 actually writes the client) plus standalone output

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:13:37 +08:00

279 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =============================================================
// 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 // 封号
}