All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m45s
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>
279 lines
10 KiB
Plaintext
279 lines
10 KiB
Plaintext
// =============================================================
|
||
// 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 // 封号
|
||
}
|