mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
874 lines
26 KiB
Plaintext
874 lines
26 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
enum Role {
|
|
ADMIN
|
|
USER
|
|
}
|
|
|
|
enum UserStatus {
|
|
ACTIVE
|
|
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
|
|
}
|
|
|
|
enum SubscriptionRiskReviewStatus {
|
|
OPEN
|
|
ACKNOWLEDGED
|
|
RESOLVED
|
|
}
|
|
|
|
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? @db.Decimal(10, 2)
|
|
renewalPricingMode String @default("FIXED_DURATION")
|
|
renewalDurationDays Int?
|
|
renewalMinDays Int?
|
|
renewalMaxDays Int?
|
|
renewalTrafficGb Int?
|
|
topupPricingMode String @default("PER_GB")
|
|
topupPricePerGb Decimal? @db.Decimal(10, 2)
|
|
topupFixedPrice Decimal? @db.Decimal(10, 2)
|
|
minTopupGb Int?
|
|
maxTopupGb Int?
|
|
streamingServiceId String?
|
|
categoryId String?
|
|
pricingMode PlanPricingMode @default(TRAFFIC_SLIDER)
|
|
fixedTrafficGb Int?
|
|
fixedPrice Decimal? @db.Decimal(10, 2)
|
|
// STREAMING: fixed price per slot
|
|
price Decimal? @db.Decimal(10, 2)
|
|
// PROXY: linked to a node, price per GB, slider purchase
|
|
nodeId String?
|
|
inboundId String?
|
|
pricePerGb Decimal? @db.Decimal(10, 2)
|
|
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?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([subscriptionId, createdAt])
|
|
@@index([userId, kind, createdAt])
|
|
@@index([level, createdAt])
|
|
@@index([reason, createdAt])
|
|
@@index([reviewStatus, createdAt])
|
|
@@index([reviewedById])
|
|
}
|
|
|
|
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 @db.Decimal(10, 2)
|
|
amount Decimal @db.Decimal(10, 2)
|
|
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 @db.Decimal(10, 2)
|
|
subtotalAmount Decimal @default(0) @db.Decimal(10, 2)
|
|
discountAmount Decimal @default(0) @db.Decimal(10, 2)
|
|
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 @db.Decimal(10, 2)
|
|
thresholdAmount Decimal? @db.Decimal(10, 2)
|
|
maxDiscountAmount Decimal? @db.Decimal(10, 2)
|
|
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 @db.Decimal(10, 2)
|
|
discountAmount Decimal @db.Decimal(10, 2)
|
|
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) @db.Decimal(10, 2)
|
|
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?
|
|
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)
|
|
subscriptionRiskIpLimitPerHour Int @default(180)
|
|
subscriptionRiskTokenLimitPerHour Int @default(60)
|
|
inviteRewardCouponId String?
|
|
inviteRewardRate Decimal @default(0) @db.Decimal(5, 2)
|
|
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])
|
|
}
|