Files
J-Board-Lite/prisma/schema.prisma
2026-04-30 09:18:05 +10:00

905 lines
27 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
}
enum Role {
ADMIN
USER
}
enum UserStatus {
ACTIVE
PENDING_EMAIL
DISABLED
BANNED
}
enum EmailTokenPurpose {
REGISTRATION_VERIFY
PASSWORD_RESET
EMAIL_CHANGE
}
enum SubscriptionType {
STREAMING
PROXY
}
enum SubscriptionStatus {
ACTIVE
EXPIRED
CANCELLED
SUSPENDED
}
enum SubscriptionAccessKind {
SINGLE
AGGREGATE
}
enum SubscriptionRiskLevel {
WARNING
SUSPENDED
}
enum SubscriptionRiskReason {
CITY_VARIANCE_WARNING
CITY_VARIANCE_SUSPEND
REGION_VARIANCE_WARNING
REGION_VARIANCE_SUSPEND
COUNTRY_VARIANCE_WARNING
COUNTRY_VARIANCE_SUSPEND
NODE_ACCESS_VOLUME_WARNING
NODE_ACCESS_VOLUME_SUSPEND
NODE_ACCESS_TARGET_WARNING
NODE_ACCESS_TARGET_SUSPEND
}
enum SubscriptionRiskReviewStatus {
OPEN
ACKNOWLEDGED
RESOLVED
}
enum SubscriptionRiskFinalAction {
RESTORE_ACCESS
KEEP_RESTRICTED
}
enum PlanPricingMode {
TRAFFIC_SLIDER
FIXED_PACKAGE
}
enum OrderStatus {
PENDING
PAID
CANCELLED
REFUNDED
}
enum OrderKind {
NEW_PURCHASE
RENEWAL
TRAFFIC_TOPUP
}
enum Protocol {
VMESS
VLESS
TROJAN
SHADOWSOCKS
HYSTERIA2
}
enum NotificationType {
ORDER
SUBSCRIPTION
TRAFFIC
SYSTEM
}
enum NotificationLevel {
INFO
SUCCESS
WARNING
ERROR
}
enum AnnouncementAudience {
PUBLIC
USERS
ADMINS
SPECIFIC_USER
}
enum AnnouncementDisplayType {
INLINE
BIG
POPUP
}
enum TaskKind {
REMINDER_DISPATCH
ORDER_PROVISION_RETRY
}
enum TaskStatus {
PENDING
RUNNING
SUCCESS
FAILED
}
enum OrderReviewStatus {
NORMAL
FLAGGED
RESOLVED
}
enum CouponDiscountType {
AMOUNT_OFF
PERCENT_OFF
}
enum InviteRewardStatus {
ISSUED
CANCELLED
}
enum SupportTicketStatus {
OPEN
USER_REPLIED
ADMIN_REPLIED
CLOSED
}
enum SupportTicketPriority {
LOW
NORMAL
HIGH
URGENT
}
model User {
id String @id @default(cuid())
email String @unique
emailVerifiedAt DateTime?
password String
name String?
role Role @default(USER)
status UserStatus @default(ACTIVE)
inviteCode String? @unique
invitedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscriptions UserSubscription[]
orders Order[]
cartItems ShoppingCartItem[]
couponGrants CouponGrant[]
inviteRewardLedgers InviteRewardLedger[] @relation("InviteRewardInviter")
inviteeRewardLedgers InviteRewardLedger[] @relation("InviteRewardInvitee")
streamingSlots StreamingSlot[]
nodeClients NodeClient[]
notifications UserNotification[]
auditLogs AuditLog[] @relation("AuditActor")
invitedBy User? @relation("UserInvites", fields: [invitedById], references: [id], onDelete: SetNull)
invitedUsers User[] @relation("UserInvites")
createdAnnouncements Announcement[] @relation("AnnouncementCreator")
receivedAnnouncements Announcement[] @relation("AnnouncementTarget")
taskRuns TaskRun[] @relation("TaskTriggeredBy")
supportTickets SupportTicket[]
supportReplies SupportTicketReply[]
emailTokens EmailToken[]
}
model EmailToken {
id String @id @default(cuid())
userId String?
email String
tokenHash String @unique
purpose EmailTokenPurpose
expiresAt DateTime
consumedAt DateTime?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([email, purpose, createdAt])
@@index([userId, purpose, createdAt])
@@index([expiresAt])
}
model SubscriptionCategory {
id String @id @default(cuid())
name String
type SubscriptionType
description String?
sortOrder Int @default(100)
accent String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plans SubscriptionPlan[]
@@index([type, isActive, sortOrder])
}
model SubscriptionPlan {
id String @id @default(cuid())
name String
type SubscriptionType
description String?
durationDays Int
isActive Boolean @default(true)
isFeatured Boolean @default(false)
recommendationLabel String?
recommendationReason String?
sortOrder Int @default(100)
totalLimit Int?
perUserLimit Int?
totalTrafficGb Int?
allowRenewal Boolean @default(false)
allowTrafficTopup Boolean @default(false)
renewalPrice Decimal?
renewalPricingMode String @default("FIXED_DURATION")
renewalDurationDays Int?
renewalMinDays Int?
renewalMaxDays Int?
renewalTrafficGb Int?
topupPricingMode String @default("PER_GB")
topupPricePerGb Decimal?
topupFixedPrice Decimal?
minTopupGb Int?
maxTopupGb Int?
streamingServiceId String?
categoryId String?
pricingMode PlanPricingMode @default(TRAFFIC_SLIDER)
fixedTrafficGb Int?
fixedPrice Decimal?
// STREAMING: fixed price per slot
price Decimal?
// PROXY: linked to a node, price per GB, slider purchase
nodeId String?
inboundId String?
pricePerGb Decimal?
minTrafficGb Int? @default(10)
maxTrafficGb Int? @default(1000)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
node NodeServer? @relation(fields: [nodeId], references: [id])
category SubscriptionCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
inbound NodeInbound? @relation(fields: [inboundId], references: [id])
streamingService StreamingService? @relation(fields: [streamingServiceId], references: [id])
inboundOptions PlanInboundOption[]
subscriptions UserSubscription[]
orders Order[]
cartItems ShoppingCartItem[]
orderItems OrderItem[]
@@index([type, isActive, isFeatured, sortOrder])
@@index([inboundId])
@@index([streamingServiceId])
@@index([categoryId])
}
model UserSubscription {
id String @id @default(cuid())
userId String
planId String
downloadToken String @unique @default(cuid())
status SubscriptionStatus @default(ACTIVE)
startDate DateTime @default(now())
endDate DateTime
trafficUsed BigInt @default(0)
trafficLimit BigInt?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
plan SubscriptionPlan @relation(fields: [planId], references: [id])
streamingSlot StreamingSlot?
nodeClient NodeClient?
createOrder Order? @relation("OrderCreatedSubscription")
targetOrders Order[] @relation("OrderTargetSubscription")
@@index([userId])
@@index([status])
}
model SubscriptionAccessLog {
id String @id @default(cuid())
userId String?
subscriptionId String?
kind SubscriptionAccessKind
ip String
userAgent String?
country String?
region String?
regionCode String?
city String?
latitude String?
longitude String?
geoSource String?
allowed Boolean @default(true)
reason String?
createdAt DateTime @default(now())
@@index([subscriptionId, createdAt])
@@index([userId, kind, createdAt])
@@index([ip, createdAt])
@@index([kind, createdAt])
}
model SubscriptionRiskEvent {
id String @id @default(cuid())
userId String?
subscriptionId String?
kind SubscriptionAccessKind
level SubscriptionRiskLevel
reason SubscriptionRiskReason
ip String?
countryCount Int @default(0)
regionCount Int @default(0)
cityCount Int @default(0)
countryKeys Json?
regionKeys Json?
cityKeys Json?
message String
dedupeKey String @unique
windowStartedAt DateTime
reviewStatus SubscriptionRiskReviewStatus @default(OPEN)
reviewNote String?
reviewedAt DateTime?
reviewedById String?
reviewedByEmail String?
riskReport String?
reportGeneratedAt DateTime?
reportSentAt DateTime?
userRestrictionActive Boolean @default(false)
userRestrictionResolvedAt DateTime?
finalAction SubscriptionRiskFinalAction?
finalActionAt DateTime?
finalActionById String?
finalActionByEmail String?
createdAt DateTime @default(now())
@@index([subscriptionId, createdAt])
@@index([userId, kind, createdAt])
@@index([level, createdAt])
@@index([reason, createdAt])
@@index([reviewStatus, createdAt])
@@index([reviewedById])
@@index([userRestrictionActive, reportSentAt])
@@index([finalAction, finalActionAt])
}
model StreamingService {
id String @id @default(cuid())
name String
credentials String
maxSlots Int
usedSlots Int @default(0)
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
slots StreamingSlot[]
plans SubscriptionPlan[]
}
model StreamingSlot {
id String @id @default(cuid())
serviceId String
userId String
subscriptionId String @unique
assignedAt DateTime @default(now())
service StreamingService @relation(fields: [serviceId], references: [id])
user User @relation(fields: [userId], references: [id])
subscription UserSubscription @relation(fields: [subscriptionId], references: [id])
@@index([serviceId])
}
model NodeServer {
id String @id @default(cuid())
name String
panelUrl String?
panelUsername String?
panelPassword String?
panelType String @default("3x-ui")
agentToken String?
status String @default("active")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
inbounds NodeInbound[]
plans SubscriptionPlan[]
routeTraces RouteTrace[]
latencies NodeLatency[]
latencyLogs NodeLatencyLog[]
}
model RouteTrace {
id String @id @default(cuid())
nodeId String
carrier String
hops Json
summary String
hopCount Int
updatedAt DateTime @updatedAt
node NodeServer @relation(fields: [nodeId], references: [id], onDelete: Cascade)
@@unique([nodeId, carrier])
@@index([nodeId])
}
model NodeInbound {
id String @id @default(cuid())
serverId String
panelInboundId Int?
protocol Protocol
port Int
tag String
settings Json
streamSettings Json?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
server NodeServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
clients NodeClient[]
plans SubscriptionPlan[]
planOptions PlanInboundOption[]
selectedByOrders Order[]
cartItems ShoppingCartItem[]
orderItems OrderItem[]
@@unique([serverId, tag])
@@unique([serverId, panelInboundId])
@@index([serverId])
}
model PlanInboundOption {
id String @id @default(cuid())
planId String
inboundId String
createdAt DateTime @default(now())
plan SubscriptionPlan @relation(fields: [planId], references: [id], onDelete: Cascade)
inbound NodeInbound @relation(fields: [inboundId], references: [id], onDelete: Cascade)
@@unique([planId, inboundId])
@@index([planId])
@@index([inboundId])
}
model ShoppingCartItem {
id String @id @default(cuid())
userId String
planId String
selectedInboundId String?
trafficGb Int?
quantity Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
plan SubscriptionPlan @relation(fields: [planId], references: [id], onDelete: Cascade)
selectedInbound NodeInbound? @relation(fields: [selectedInboundId], references: [id], onDelete: SetNull)
@@index([userId, createdAt])
@@index([planId])
}
model OrderItem {
id String @id @default(cuid())
orderId String
planId String
selectedInboundId String?
trafficGb Int?
quantity Int @default(1)
unitAmount Decimal
amount Decimal
createdAt DateTime @default(now())
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
plan SubscriptionPlan @relation(fields: [planId], references: [id])
selectedInbound NodeInbound? @relation(fields: [selectedInboundId], references: [id], onDelete: SetNull)
@@index([orderId])
@@index([planId])
}
model NodeClient {
id String @id @default(cuid())
inboundId String
userId String
subscriptionId String @unique
email String
uuid String
trafficUp BigInt @default(0)
trafficDown BigInt @default(0)
expiryTime DateTime?
isEnabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
inbound NodeInbound @relation(fields: [inboundId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
subscription UserSubscription @relation(fields: [subscriptionId], references: [id])
@@index([inboundId])
@@index([userId])
}
model Order {
id String @id @default(cuid())
userId String
planId String
kind OrderKind @default(NEW_PURCHASE)
selectedInboundId String?
targetSubscriptionId String?
subscriptionId String? @unique
amount Decimal
subtotalAmount Decimal @default(0)
discountAmount Decimal @default(0)
couponId String?
couponCode String?
promotionName String?
trafficGb Int?
durationDays Int?
status OrderStatus @default(PENDING)
paymentMethod String?
paymentRef String?
paymentUrl String?
tradeNo String? @unique
expireAt DateTime?
note String?
reviewStatus OrderReviewStatus @default(NORMAL)
reviewNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
plan SubscriptionPlan @relation(fields: [planId], references: [id])
selectedInbound NodeInbound? @relation(fields: [selectedInboundId], references: [id])
targetSubscription UserSubscription? @relation("OrderTargetSubscription", fields: [targetSubscriptionId], references: [id])
subscription UserSubscription? @relation("OrderCreatedSubscription", fields: [subscriptionId], references: [id])
coupon Coupon? @relation(fields: [couponId], references: [id], onDelete: SetNull)
items OrderItem[]
couponGrants CouponGrant[] @relation("CouponGrantUsedOrder")
inviteRewards InviteRewardLedger[]
@@index([userId])
@@index([kind])
@@index([targetSubscriptionId])
@@index([status])
@@index([tradeNo])
@@index([reviewStatus])
@@index([couponId])
}
model NodeLatency {
id String @id @default(cuid())
nodeId String
carrier String
latencyMs Int
checkedAt DateTime @default(now())
node NodeServer @relation(fields: [nodeId], references: [id], onDelete: Cascade)
@@unique([nodeId, carrier])
@@index([nodeId])
}
model NodeLatencyLog {
id String @id @default(cuid())
nodeId String
carrier String
latencyMs Int
checkedAt DateTime @default(now())
node NodeServer @relation(fields: [nodeId], references: [id], onDelete: Cascade)
@@index([nodeId, carrier, checkedAt])
}
model Coupon {
id String @id @default(cuid())
code String @unique
name String
description String?
discountType CouponDiscountType @default(AMOUNT_OFF)
discountValue Decimal
thresholdAmount Decimal?
maxDiscountAmount Decimal?
totalLimit Int?
perUserLimit Int?
isPublic Boolean @default(true)
isActive Boolean @default(true)
startsAt DateTime?
endsAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
grants CouponGrant[]
@@index([isActive, startsAt, endsAt])
}
model CouponGrant {
id String @id @default(cuid())
couponId String
userId String
source String?
sourceOrderId String?
usedOrderId String?
createdAt DateTime @default(now())
usedAt DateTime?
coupon Coupon @relation(fields: [couponId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
usedOrder Order? @relation("CouponGrantUsedOrder", fields: [usedOrderId], references: [id], onDelete: SetNull)
@@index([userId, usedAt])
@@index([couponId])
}
model PromotionRule {
id String @id @default(cuid())
name String
thresholdAmount Decimal
discountAmount Decimal
isActive Boolean @default(true)
startsAt DateTime?
endsAt DateTime?
sortOrder Int @default(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive, thresholdAmount, sortOrder])
}
model InviteRewardLedger {
id String @id @default(cuid())
inviterId String
inviteeId String
orderId String
rewardAmount Decimal @default(0)
couponCode String?
status InviteRewardStatus @default(ISSUED)
createdAt DateTime @default(now())
inviter User @relation("InviteRewardInviter", fields: [inviterId], references: [id], onDelete: Cascade)
invitee User @relation("InviteRewardInvitee", fields: [inviteeId], references: [id], onDelete: Cascade)
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
@@unique([orderId, inviterId])
@@index([inviterId, createdAt])
@@index([inviteeId, createdAt])
}
model PaymentConfig {
id String @id @default(cuid())
provider String @unique
enabled Boolean @default(false)
config Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model TrafficLog {
id String @id @default(cuid())
clientId String
upload BigInt
download BigInt
timestamp DateTime @default(now())
@@index([clientId])
@@index([timestamp])
}
model UserNotification {
id String @id @default(cuid())
userId String
type NotificationType
level NotificationLevel @default(INFO)
title String
body String
link String?
isRead Boolean @default(false)
readAt DateTime?
dedupeKey String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, isRead, createdAt])
@@index([type, createdAt])
}
model AuditLog {
id String @id @default(cuid())
actorUserId String?
actorEmail String?
actorRole Role?
action String
targetType String
targetId String?
targetLabel String?
message String
metadata Json?
createdAt DateTime @default(now())
actor User? @relation("AuditActor", fields: [actorUserId], references: [id], onDelete: SetNull)
@@index([actorUserId, createdAt])
@@index([action, createdAt])
@@index([targetType, targetId])
@@index([createdAt])
}
model AppConfig {
id String @id @default("default")
siteName String @default("J-Board")
siteUrl String?
subscriptionUrl String?
allowRegistration Boolean @default(true)
emailVerificationRequired Boolean @default(false)
requireInviteCode Boolean @default(false)
supportContact String?
supportOpenTicketLimit Int @default(2)
maintenanceNotice String?
siteNotice String?
autoReminderDispatchEnabled Boolean @default(true)
reminderDispatchIntervalMinutes Int @default(60)
trafficSyncEnabled Boolean @default(true)
trafficSyncIntervalSeconds Int @default(60)
subscriptionRiskEnabled Boolean @default(true)
subscriptionRiskAutoSuspend Boolean @default(true)
subscriptionRiskWindowHours Int @default(24)
subscriptionRiskCityWarning Int @default(4)
subscriptionRiskCitySuspend Int @default(5)
subscriptionRiskRegionWarning Int @default(2)
subscriptionRiskRegionSuspend Int @default(3)
subscriptionRiskCountryWarning Int @default(2)
subscriptionRiskCountrySuspend Int @default(3)
subscriptionRiskIpLimitPerHour Int @default(180)
subscriptionRiskTokenLimitPerHour Int @default(60)
nodeAccessRiskEnabled Boolean @default(true)
nodeAccessConnectionWarning Int @default(180)
nodeAccessConnectionSuspend Int @default(360)
nodeAccessUniqueTargetWarning Int @default(80)
nodeAccessUniqueTargetSuspend Int @default(160)
inviteRewardCouponId String?
inviteRewardRate Decimal @default(0)
inviteRewardEnabled Boolean @default(false)
turnstileSiteKey String?
turnstileSecretKey String?
smtpEnabled Boolean @default(false)
smtpHost String?
smtpPort Int @default(587)
smtpSecure Boolean @default(false)
smtpUser String?
smtpPassword String?
smtpFromName String?
smtpFromEmail String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Announcement {
id String @id @default(cuid())
title String
body String
audience AnnouncementAudience @default(USERS)
displayType AnnouncementDisplayType @default(INLINE)
targetUserId String?
createdById String?
isActive Boolean @default(true)
dismissible Boolean @default(true)
sendNotification Boolean @default(true)
startAt DateTime?
endAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
targetUser User? @relation("AnnouncementTarget", fields: [targetUserId], references: [id], onDelete: SetNull)
createdBy User? @relation("AnnouncementCreator", fields: [createdById], references: [id], onDelete: SetNull)
@@index([audience, isActive, createdAt])
@@index([targetUserId, isActive])
@@index([startAt, endAt, isActive])
}
model TaskRun {
id String @id @default(cuid())
kind TaskKind
status TaskStatus @default(PENDING)
title String
targetType String?
targetId String?
payload Json?
result Json?
errorMessage String?
retryable Boolean @default(false)
retryCount Int @default(0)
triggeredById String?
startedAt DateTime?
finishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
triggeredBy User? @relation("TaskTriggeredBy", fields: [triggeredById], references: [id], onDelete: SetNull)
@@index([kind, createdAt])
@@index([status, createdAt])
@@index([targetType, targetId])
}
model SupportTicket {
id String @id @default(cuid())
userId String
subject String
category String?
status SupportTicketStatus @default(OPEN)
priority SupportTicketPriority @default(NORMAL)
lastReplyAt DateTime?
closedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
replies SupportTicketReply[]
attachments SupportTicketAttachment[]
@@index([userId, status, createdAt])
@@index([status, priority, updatedAt])
}
model SupportTicketReply {
id String @id @default(cuid())
ticketId String
authorUserId String?
isAdmin Boolean @default(false)
body String
createdAt DateTime @default(now())
ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
author User? @relation(fields: [authorUserId], references: [id], onDelete: SetNull)
attachments SupportTicketAttachment[]
@@index([ticketId, createdAt])
@@index([authorUserId, createdAt])
}
model SupportTicketAttachment {
id String @id @default(cuid())
ticketId String
replyId String
fileName String
mimeType String
size Int
content Bytes
createdAt DateTime @default(now())
ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
reply SupportTicketReply @relation(fields: [replyId], references: [id], onDelete: Cascade)
@@index([ticketId, createdAt])
@@index([replyId, createdAt])
}