// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] } datasource db { provider = "postgresql" } // ==================== 枚举类型 ==================== enum UserRole { admin user } enum UserStatus { active banned } enum HostStatus { online offline maintenance } enum HostAddressKind { domain ipv4 ipv6 } enum HostAddressSource { input resolved } enum HostAddressCheckTrigger { create update backfill poll } enum HostAddressCheckStatus { success failed } enum HostAddressConflictStatus { active resolved } enum InstanceType { container vm both } enum NetworkMode { nat // 纯 IPv4 NAT nat_ipv6 // IPv4 NAT + IPv6 独立(Routed) nat_ipv6_nat // IPv4 NAT + IPv6 NAT(共享宿主机 IPv6) ipv6_only // 纯 IPv6 独立(Routed),无 NAT 端口映射 ipv6_nat // 纯 IPv6 NAT(共享宿主机 IPv6),无 NAT 端口映射 } // 存储池用途枚举 enum StoragePurpose { instance_data // 用于实例数据盘(创建实例时默认存储) instance_storage // 用于实例存储盘(可手动挂载) } // 反代站点状态枚举 enum ProxySiteStatus { pending // 等待 DNS 解析 active // 已生效 error // 配置失败 } enum InstanceStatus { creating running stopped suspended // 封停状态(到期或手动封停) error deleted } enum Protocol { tcp udp } enum BackupStatus { creating ready error deleted } enum RestoreTaskStatus { PENDING PROCESSING COMPLETED FAILED } // 存储类型枚举 enum StorageType { S3 // 预留 WEBDAV FTP SFTP } // 备份上传任务状态 enum BackupUploadTaskStatus { PENDING PROCESSING COMPLETED FAILED } // 实例操作任务类型 enum InstanceTaskType { start stop restart rebuild clone recreate // 重建:创建新实例替换旧实例 change_host // 改节点:在其他节点重建实例并保留数据库实例 ID } // 实例操作任务状态 enum InstanceTaskStatus { PENDING PROCESSING COMPLETED FAILED } enum HostNotificationEmailTaskStatus { PENDING PROCESSING SENT FAILED } // ==================== 恢复任务模型 ==================== model RestoreTask { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") backupId Int? @map("backup_id") hostId Int @map("host_id") userId Int @map("user_id") status RestoreTaskStatus @default(PENDING) progress String? // 当前执行步骤: stopping, restoring, cleaning, replacing, starting retryCount Int @default(0) @map("retry_count") // 重试次数 tempInstanceName String? @map("temp_instance_name") originalInstanceName String @map("original_instance_name") originalIncusId String @map("original_incus_id") error String? createdAt DateTime @default(now()) @map("created_at") startedAt DateTime? @map("started_at") finishedAt DateTime? @map("finished_at") @@index([hostId, status]) @@index([instanceId]) @@map("restore_tasks") } // ==================== 实例操作任务模型 ==================== model InstanceTask { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") hostId Int @map("host_id") userId Int @map("user_id") taskType InstanceTaskType @map("task_type") status InstanceTaskStatus @default(PENDING) progress String? // 当前执行步骤 retryCount Int @default(0) @map("retry_count") // 重装/克隆操作特有字段 imageAlias String? @map("image_alias") // 重装使用的镜像别名 sshKeyId Int? @map("ssh_key_id") // 重装使用的 SSH 密钥 ID customInitCommandIds String? @map("custom_init_command_ids") // 重装使用的自定义初始化命令ID列表 (JSON数组) targetName String? @map("target_name") // 克隆的目标名称 targetHostId Int? @map("target_host_id") // 克隆的目标宿主机 snapshotName String? @map("snapshot_name") // 克隆时的快照名称 newInstanceId Int? @map("new_instance_id") // 克隆后的新实例 ID error String? createdAt DateTime @default(now()) @map("created_at") startedAt DateTime? @map("started_at") finishedAt DateTime? @map("finished_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@index([hostId, status]) @@index([instanceId]) @@index([userId]) @@map("instance_tasks") } enum NotificationChannelType { telegram discord email webhook } enum NotificationLogStatus { pending sent failed } enum OAuthProvider { github google } // ==================== 计费相关枚举 ==================== // 余额变动类型 enum BalanceLogType { recharge // 充值 consume // 消费(开通/续费实例) refund // 退款 admin_adjust // 管理员调整 gift // 赠送 transfer_fee // 转移手续费 transfer_refund // 转移手续费退还 hosting_withdraw // 托管余额提现 hosting_deduction // 托管实例销毁扣款(从面板余额补扣) invite_generate // 生成邀请码扣款 } enum VipBenefitRewardType { balance points instance } enum VipBenefitClaimStatus { delivered pending } // 支付渠道类型 enum PaymentProviderType { yipay // 易支付 heleket // Heleket 加密支付 stripe // Stripe alipay_direct // 支付宝直连(预留) wechat_direct // 微信直连(预留) manual // 人工充值 } // 支付渠道状态 enum PaymentProviderStatus { active // 启用 disabled // 禁用 testing // 测试模式 } // 充值记录状态 enum RechargeStatus { pending // 待支付 paid // 已支付,处理中 completed // 已完成(余额已到账) failed // 失败 cancelled // 已取消 refunded // 已退款 } enum VipLevelRuleType { user hosting } enum VipLevelConditionMode { any all } // 实例计费记录类型 enum BillingRecordType { newPurchase // 新开 renew // 续费 upgrade // 升级 downgrade // 降级 refund // 退款(仅管理员操作) transfer_fee // 转移手续费 } // AFF余额变动类型 enum AffLogType { new_purchase // 新购返利 renew // 续费返利 convert // 转化为主余额 } // AFF转化申请状态 enum AffWithdrawalStatus { pending // 待审核 approved // 已通过 rejected // 已拒绝 } // 流量状态枚举 enum TrafficStatus { NORMAL // 正常 WARNING // 已发送 80% 预警 LIMITED // 已限速 } // 好友关系状态枚举 enum FriendshipStatus { pending // 待确认 accepted // 已接受 rejected // 已拒绝 removed // 已删除(双方都已不是好友,但保留历史记录) } // ==================== 用户相关模型 ==================== model User { id Int @id @default(autoincrement()) username String @unique email String? passwordHash String @map("password_hash") role UserRole @default(user) status UserStatus @default(active) banReason String? @map("ban_reason") // 封禁原因(管理员可选填写) balance Decimal @default(0) @db.Decimal(10, 2) avatarStyle String @default("bigSmile") @map("avatar_style") // bigSmile, croodles, notionists, lorelei avatarBadgeId String? @map("avatar_badge_id") hasCreatedHostBefore Boolean @default(false) @map("has_created_host_before") // 2FA 双因素认证 twoFactorEnabled Boolean @default(false) @map("two_factor_enabled") twoFactorSecret String? @map("two_factor_secret") twoFactorRecoveryCodes String? @map("two_factor_recovery_codes") // JSON array of encrypted codes createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关系 quota UserQuota? inviteCodes InviteCode[] @relation("CreatedInviteCodes") usedInviteCodes InviteCode[] @relation("UsedInviteCodes") sshKeys SshKey[] instances Instance[] notificationChannels NotificationChannel[] storageConfigs StorageConfig[] oauthBindings UserOAuthBinding[] telegramBinding UserTelegramBinding? telegramBindTokens TelegramBindToken[] helpArticles HelpArticle[] @relation("CreatedArticles") logs Log[] transfersSent InstanceTransfer[] @relation("TransferFrom") transfersReceived InstanceTransfer[] @relation("TransferTo") refreshTokens RefreshToken[] tokenInvalidations TokenInvalidation[] // 用户资源关系 hosts Host[] packages Package[] // 好友关系 friendshipsSent Friendship[] @relation("FriendshipSent") friendshipsReceived Friendship[] @relation("FriendshipReceived") // 套餐共享关系 packageSharesOwned PackageShare[] @relation("PackageShareOwner") packageSharesReceived PackageShare[] @relation("PackageShareReceiver") // 敏感操作验证 operationVerifications OperationVerification[] // 站内信 inboxMessages InboxMessage[] hostNotificationEmailTasks HostNotificationEmailTask[] // 登录记录 loginRecords LoginRecord[] // 公告发送记录 announcements Announcement[] @relation("AnnouncementSender") // 工单系统 ticketsCreated Ticket[] @relation("TicketCreator") ticketMessages TicketMessage[] @relation("TicketMessageSender") ticketAttachments TicketMessageAttachment[] @relation("TicketAttachmentUploader") // 签到系统 checkinRecords CheckinRecord[] @relation("CheckinRecords") redeemRecords CheckinRecord[] @relation("RedeemRecords") checkinStats CheckinStats? // 系统兑换码 redeemCodesCreated RedeemCode[] @relation("RedeemCodesCreated") redeemCodeUsages RedeemCodeUsage[] @relation("RedeemCodeUsages") // 用户自定义初始化命令 customInitCommands CustomInitCommand[] // 用户终端快捷命令 terminalSavedCommands TerminalSavedCommand[] // 计费相关 balanceLogs BalanceLog[] rechargeRecords RechargeRecord[] badgeOwnerships UserBadgeOwnership[] vipBenefitClaims VipBenefitClaim[] // AFF推荐计划 affBalance Decimal @default(0) @map("aff_balance") @db.Decimal(10, 2) // AFF余额 affActivatedAt DateTime? @map("aff_activated_at") // AFF激活时间 affCodes AffCode[] affLogs AffLog[] affWithdrawals AffWithdrawal[] // 托管余额系统 hostingBalance Decimal @default(0) @map("hosting_balance") @db.Decimal(10, 2) // 可用托管余额 hostingBalanceLogs HostingBalanceLog[] hostingWithdrawals HostingWithdrawal[] hostingBlocksCreated HostingUserBlock[] @relation("HostingBlocker") hostingBlocksReceived HostingUserBlock[] @relation("HostingBlockedUser") hostingZone HostingZone? @relation("HostingZoneOwner") // 域名邮箱系统 mailSubscriptions MailSubscription[] // 抽奖系统 userPoints UserPoints? pointsLogs PointsLog[] lotteryRecords LotteryRecord[] // 资源池系统 resourcePool UserResourcePool? resourcePoolLogs ResourcePoolLog[] // 销毁记录 destroyRecords UserDestroyRecord[] instanceAuditRules InstanceAuditRule[] @relation("InstanceAuditRuleCreator") instanceAuditBuiltinRuleOverrides InstanceAuditBuiltinRuleOverride[] @relation("InstanceAuditBuiltinRuleOverrideCreator") instanceAuditIgnores InstanceAuditIgnore[] @relation("InstanceAuditIgnoreCreator") instanceAuditScans InstanceAuditScan[] @relation("InstanceAuditScanUser") instanceAuditActions InstanceAuditAction[] @relation("InstanceAuditActionUser") @@map("users") } model UserQuota { id Int @id @default(autoincrement()) userId Int @unique @map("user_id") // 新配额系统:控制用户可拥有的资源数量 // 默认值为 0,表示功能未开放,需要管理员授权才能使用 // 注意:不再限制实例配额,用户可以创建无限数量的实例 hostLimit Int @default(0) @map("host_limit") // 宿主机数量上限(0 = 未授权) hostUsed Int @default(0) @map("host_used") // 已使用宿主机数量 friendLimit Int @default(0) @map("friend_limit") // 好友数量上限(0 = 未授权) friendUsed Int @default(0) @map("friend_used") // 已使用好友数量 packageLimit Int @default(0) @map("package_limit") // 套餐数量上限(0 = 未授权) packageUsed Int @default(0) @map("package_used") // 已使用套餐数量 // 流量控制 (单位: Bytes) monthlyTrafficLimit BigInt? @map("monthly_traffic_limit") // null = 无限制 monthlyTrafficUsed BigInt @default(0) @map("monthly_traffic_used") trafficStatus TrafficStatus @default(NORMAL) @map("traffic_status") trafficWarningSentAt DateTime? @map("traffic_warning_sent_at") // 流量包扩展 (预留) extraTrafficQuota BigInt @default(0) @map("extra_traffic_quota") extraTrafficUsed BigInt @default(0) @map("extra_traffic_used") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_quotas") } model InviteCode { id Int @id @default(autoincrement()) code String @unique createdBy Int @map("created_by") usedBy Int? @map("used_by") usedAt DateTime? @map("used_at") expiresAt DateTime? @map("expires_at") costSnapshot Json? @map("cost_snapshot") createdAt DateTime @default(now()) @map("created_at") creator User @relation("CreatedInviteCodes", fields: [createdBy], references: [id]) user User? @relation("UsedInviteCodes", fields: [usedBy], references: [id]) @@index([code]) @@map("invite_codes") } model SshKey { id Int @id @default(autoincrement()) userId Int @map("user_id") name String publicKey String @map("public_key") fingerprint String createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("ssh_keys") } // ==================== 会话管理模型 ==================== model RefreshToken { id Int @id @default(autoincrement()) token String @unique // Refresh Token 完整值 userId Int @map("user_id") username String role UserRole ip String? // 登录 IP userAgent String? @map("user_agent") // 用户代理 createdAt DateTime @default(now()) @map("created_at") expiresAt DateTime @map("expires_at") // 过期时间 lastActiveAt DateTime @default(now()) @updatedAt @map("last_active_at") // 最后活跃时间 user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([token]) @@index([expiresAt]) // 用于清理过期 token @@map("refresh_tokens") } // ==================== Token 失效标记模型 ==================== model TokenInvalidation { id Int @id @default(autoincrement()) userId Int @map("user_id") sessionId String? @map("session_id") // 会话ID(Refresh Token 前缀),null 表示用户级别的失效 invalidatedAt Int @map("invalidated_at") // 失效时间戳(秒级,与 JWT iat 一致) createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, sessionId]) // 每个用户每个会话只能有一条失效记录(sessionId 为 null 时表示用户级别) @@index([userId]) @@index([sessionId]) @@index([invalidatedAt]) // 用于清理过期记录 @@map("token_invalidations") } // ==================== 好友关系模型 ==================== model Friendship { id Int @id @default(autoincrement()) userId Int @map("user_id") // 发起者 friendId Int @map("friend_id") // 接收者 status FriendshipStatus @default(pending) remark String? // 申请备注 createdAt DateTime @default(now()) @map("created_at") acceptedAt DateTime? @map("accepted_at") rejectedAt DateTime? @map("rejected_at") user User @relation("FriendshipSent", fields: [userId], references: [id], onDelete: Cascade) friend User @relation("FriendshipReceived", fields: [friendId], references: [id], onDelete: Cascade) @@unique([userId, friendId]) @@index([friendId, status]) @@index([userId, status]) @@map("friendships") } // ==================== 节点相关模型 ==================== model Host { id Int @id @default(autoincrement()) userId Int @map("user_id") // 所有者 name String url String location String? countryCode String @default("us") @map("country_code") architecture String @default("x86_64") status HostStatus @default(offline) certPath String? @map("cert_path") keyPath String? @map("key_path") natPublicIp String? @map("nat_public_ip") natPublicIpv6 String? @map("nat_public_ipv6") // 宿主机公网 IPv6(用于 nat_ipv6_nat / ipv6_nat 模式) natBindIp String? @map("nat_bind_ip") // 端口映射实际监听的 IPv4 地址 natBindIpv6 String? @map("nat_bind_ipv6") // 端口映射实际监听的 IPv6 地址 natPortStart Int? @map("nat_port_start") natPortEnd Int? @map("nat_port_end") cpuUsed Int @default(0) @map("cpu_used") memoryUsed Int @default(0) @map("memory_used") diskUsed Int @default(0) @map("disk_used") tags Json? @default("[]") natPortsUsedCount Int @default(0) @map("nat_ports_used_count") cpuAllowanceMax Int @default(0) @map("cpu_allowance_max") memoryMax Int @default(0) @map("memory_max") instanceType InstanceType @default(container) @map("instance_type") // 初始化配置字段 ipAddress String? @map("ip_address") // 存储配置 storageDriver String @default("zfs") @map("storage_driver") // zfs | lvm storageType String @default("loop") @map("storage_type") // loop | disk storagePath String? @map("storage_path") // 设备路径如 /dev/sdb,或 loop 模式下为空 storageSize Int @default(60) @map("storage_size") // loop 模式下的大小 (GB) // 网络配置 ipv6Mode Int @default(1) @map("ipv6_mode") // 1=Routed, 2=NAT, 3=Disabled ipv6Subnet String? @map("ipv6_subnet") ipv6Gateway String? @map("ipv6_gateway") ipv6ParentInterface String? @map("ipv6_parent_interface") // 宿主机物理网卡名 (routed 模式) // API 与内核配置 enableApi Boolean @default(true) @map("enable_api") sysctlConfig String? @map("sysctl_config") // 自定义内核参数 // 安装流程 installToken String? @unique @map("install_token") installTokenExpire DateTime? @map("install_token_expire") isInstalled Boolean @default(false) @map("is_installed") // 证书下载限制(每次安装操作独立计数) certDownloadCount Int @default(0) @map("cert_download_count") certDownloadExpire DateTime? @map("cert_download_expire") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Caddy 反代配置 caddyEnabled Boolean @default(false) @map("caddy_enabled") // 是否已安装 Caddy caddyUsername String? @map("caddy_username") // Basic Auth 用户名 caddyPassword String? @map("caddy_password") // Basic Auth 密码 caddyPort Int @default(8444) @map("caddy_port") // Caddy API 端口 // 转移控制 transferEnabled Boolean @default(true) @map("transfer_enabled") // 是否允许该宿主机下的实例进行转移 // 流量配置 trafficResetDay Int @default(1) @map("traffic_reset_day") // 流量重置日(1-28),默认每月1号 // 节点公告 announcement String? @map("announcement") // 节点公告内容,显示给该节点下的实例用户 // 节点通知设置(仅托管主外部通知) notifyPurchase Boolean @default(true) @map("notify_purchase") // 是否通知新购 notifyRenew Boolean @default(true) @map("notify_renew") // 是否通知续费 notifyDestroy Boolean @default(false) @map("notify_destroy") // 是否通知销毁/退款 // 资源池玩法 enableResourcePool Boolean @default(true) @map("enable_resource_pool") // 是否参与资源池玩法(签到/抽奖资源应用) // 探针地址 probeUrl String? @map("probe_url") // 节点探针监控地址 user User @relation(fields: [userId], references: [id], onDelete: Cascade) instances Instance[] ipAddresses IpAddress[] allowedImages HostAllowedImage[] portMappings PortMapping[] packageHosts PackageHost[] storagePools StoragePool[] proxySites ProxySite[] announcements Announcement[] @relation("HostAnnouncements") tickets Ticket[] @relation("HostTickets") redeemCodes RedeemCode[] destroyRecords UserDestroyRecord[] hostNotificationEmailTasks HostNotificationEmailTask[] addressAliases HostAddressAlias[] @relation("HostAddressAliases") addressResolutionLogs HostAddressResolutionLog[] @relation("HostAddressResolutionLogs") addressConflictsA HostAddressConflict[] @relation("HostAddressConflictA") addressConflictsB HostAddressConflict[] @relation("HostAddressConflictB") agent HostAgent? instanceAuditRules InstanceAuditRule[] instanceAuditBuiltinRuleOverrides InstanceAuditBuiltinRuleOverride[] instanceAuditIgnores InstanceAuditIgnore[] instanceAuditScans InstanceAuditScan[] instanceAuditActions InstanceAuditAction[] @@unique([userId, name]) // 同一用户下名称唯一 @@index([userId]) @@map("hosts") } model HostAgent { id Int @id @default(autoincrement()) hostId Int @unique @map("host_id") agentId String @unique @map("agent_id") secretHash String @map("secret_hash") secretEncrypted String @map("secret_encrypted") installTokenHash String? @unique @map("install_token_hash") installTokenExpiresAt DateTime? @map("install_token_expires_at") installTokenUsedAt DateTime? @map("install_token_used_at") enabled Boolean @default(true) status String @default("offline") version String? capabilities Json? @default("[]") lastReport Json? @default("{}") @map("last_report") lastSeenAt DateTime? @map("last_seen_at") lastHeartbeatIp String? @map("last_heartbeat_ip") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) nonces HostAgentNonce[] @@index([enabled]) @@index([status]) @@index([lastSeenAt]) @@index([installTokenExpiresAt]) @@map("host_agents") } model HostAgentNonce { id Int @id @default(autoincrement()) agentId String @map("agent_id") nonce String expiresAt DateTime @map("expires_at") createdAt DateTime @default(now()) @map("created_at") agent HostAgent @relation(fields: [agentId], references: [agentId], onDelete: Cascade) @@unique([agentId, nonce]) @@index([agentId]) @@index([expiresAt]) @@map("host_agent_nonces") } model HostAddressAlias { id Int @id @default(autoincrement()) hostId Int @map("host_id") address String kind HostAddressKind source HostAddressSource createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") host Host @relation("HostAddressAliases", fields: [hostId], references: [id], onDelete: Cascade) @@unique([hostId, address]) @@index([hostId]) @@index([address]) @@index([source, kind]) @@map("host_address_aliases") } model HostAddressResolutionLog { id Int @id @default(autoincrement()) hostId Int @map("host_id") inputAddress String @map("input_address") inputKind HostAddressKind @map("input_kind") trigger HostAddressCheckTrigger status HostAddressCheckStatus resolvedAddresses Json @default("[]") @map("resolved_addresses") error String? details Json? @default("{}") createdAt DateTime @default(now()) @map("created_at") host Host @relation("HostAddressResolutionLogs", fields: [hostId], references: [id], onDelete: Cascade) @@index([hostId, createdAt(sort: Desc)]) @@index([status, createdAt(sort: Desc)]) @@map("host_address_resolution_logs") } model HostAddressConflict { id Int @id @default(autoincrement()) hostAId Int @map("host_a_id") hostBId Int @map("host_b_id") address String status HostAddressConflictStatus @default(active) firstDetectedAt DateTime @default(now()) @map("first_detected_at") lastDetectedAt DateTime @updatedAt @map("last_detected_at") resolvedAt DateTime? @map("resolved_at") hostA Host @relation("HostAddressConflictA", fields: [hostAId], references: [id], onDelete: Cascade) hostB Host @relation("HostAddressConflictB", fields: [hostBId], references: [id], onDelete: Cascade) @@unique([hostAId, hostBId, address]) @@index([address]) @@index([status, lastDetectedAt(sort: Desc)]) @@map("host_address_conflicts") } // 存储池模型 model StoragePool { id Int @id @default(autoincrement()) hostId Int @map("host_id") name String // 存储池名称(与 Incus 一致) driver String // zfs | lvm | btrfs | dir purpose StoragePurpose @default(instance_data) description String? config Json? @default("{}") // 存储原始配置 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) @@unique([hostId, name]) // 同一宿主机下存储池名称唯一 @@index([hostId]) @@map("storage_pools") } // ==================== 套餐相关模型 ==================== // 套餐与宿主机的多对多关联表 model PackageHost { id Int @id @default(autoincrement()) packageId Int @map("package_id") hostId Int @map("host_id") storagePoolName String? @map("storage_pool_name") trafficMultiplier Decimal @default(1.0) @map("traffic_multiplier") @db.Decimal(8, 3) createdAt DateTime @default(now()) @map("created_at") package Package @relation(fields: [packageId], references: [id], onDelete: Cascade) host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) @@unique([packageId, hostId]) // 防止重复绑定 @@index([packageId]) @@index([hostId]) @@map("package_hosts") } model Package { id Int @id @default(autoincrement()) userId Int @map("user_id") // 所有者 name String description String? cpuMax Int @map("cpu_max") memoryMax Int @map("memory_max") diskMax Int @map("disk_max") bandwidthMax Int? @map("bandwidth_max") networkMode NetworkMode @default(nat) @map("network_mode") instanceType InstanceType @default(container) @map("instance_type") // 实例类型: 容器/虚拟机 nodeSelectors Json? @default("[]") @map("node_selectors") privileged Boolean @default(false) nested Boolean @default(false) active Boolean @default(true) // 套餐资源限制 portLimit Int @default(20) @map("port_limit") // 端口映射数上限 snapshotLimit Int @default(5) @map("snapshot_limit") // 快照数上限 backupLimit Int @default(3) @map("backup_limit") // 备份数上限 siteLimit Int @default(10) @map("site_limit") // 反代站点数上限 monthlyTrafficLimit BigInt? @map("monthly_traffic_limit") // 月流量限额 (Bytes),null = 无限制 // 存储 I/O 限制 ioLimitMode String @default("throughput") @map("io_limit_mode") // "throughput" | "iops" - IO限制模式 limitsRead String @default("100MB") @map("limits_read") // 磁盘读取吞吐限制 limitsWrite String @default("100MB") @map("limits_write") // 磁盘写入吞吐限制 limitsReadIops Int @default(500) @map("limits_read_iops") // 磁盘读取 IOPS limitsWriteIops Int @default(500) @map("limits_write_iops") // 磁盘写入 IOPS // 网络限制 limitsIngress String @default("300Mbit") @map("limits_ingress") // 网络入站带宽 limitsEgress String @default("300Mbit") @map("limits_egress") // 网络出站带宽 // 进程与调度 limitsProcesses Int @default(500) @map("limits_processes") // 进程数限制 limitsCpuPriority Int @default(10) @map("limits_cpu_priority") // CPU 调度优先级 (0-10) // 启动配置 bootAutostart Boolean @default(true) @map("boot_autostart") // 开机自启 bootAutostartPriority Int @default(20) @map("boot_autostart_priority") // 启动优先级 (0-100) bootAutostartDelay Int @default(15) @map("boot_autostart_delay") // 启动延迟 (5-600秒) bootHostShutdownTimeout Int @default(30) @map("boot_host_shutdown_timeout") // 关机超时 (30-600秒) // 全局共享配置(当 globalShared = true 时,所有用户都可以使用此套餐) globalShared Boolean @default(false) @map("global_shared") // 是否全局共享 globalQuotaMultiplier Decimal? @map("global_quota_multiplier") @db.Decimal(3, 1) // 全局共享的配额倍数限制,如 0.5, 1.0, 1.5, 2.0,null 表示无限制(1x) globalMaxInstances Int? @map("global_max_instances") // 全局共享的最大实例数,null 表示无限制 // 开通门槛:创建本套餐实例前,用户必须已持有指定前置套餐的正常实例 requiredPackageId Int? @map("required_package_id") // 实例操作权限 allowInstanceDeletion Boolean @default(true) @map("allow_instance_deletion") // 是否允许用户删除实例 // 资源释放通知渠道(可选) releaseChannelId Int? @map("release_channel_id") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) instances Instance[] shares PackageShare[] packageHosts PackageHost[] plans PackagePlan[] // 套餐下的付费方案 requiredPackage Package? @relation("PackagePrerequisite", fields: [requiredPackageId], references: [id], onDelete: NoAction) dependentPackages Package[] @relation("PackagePrerequisite") releaseChannel NotificationChannel? @relation("PackageReleaseChannel", fields: [releaseChannelId], references: [id], onDelete: SetNull) @@unique([userId, name]) // 同一用户下名称唯一 @@index([userId]) @@index([requiredPackageId]) @@map("packages") } // ==================== 实例相关模型 ==================== model Instance { id Int @id @default(autoincrement()) incusId String @map("incus_id") name String userId Int @map("user_id") hostId Int @map("host_id") packageId Int? @map("package_id") packagePlanId Int? @map("package_plan_id") // 套餐方案 ID:null 表示免费实例 storagePoolName String? @map("storage_pool_name") image String status InstanceStatus @default(creating) cpu Int memory Int disk Int ipv4 String? ipv6 String? networkMode NetworkMode @default(nat) @map("network_mode") sshPort Int? @map("ssh_port") rootPassword String? @map("root_password") snapshottedSpecs Json? @default("{}") @map("snapshotted_specs") portLimit Int? @map("port_limit") snapshotLimit Int? @map("snapshot_limit") backupLimit Int? @map("backup_limit") siteLimit Int? @map("site_limit") swapEnabled Boolean @default(false) @map("swap_enabled") swapSize Int? @map("swap_size") // SWAP 大小(MB),null/0 表示未配置 // 实例级流量控制 monthlyTrafficLimit BigInt? @map("monthly_traffic_limit") monthlyTrafficUsed BigInt @default(0) @map("monthly_traffic_used") trafficStatus TrafficStatus @default(NORMAL) @map("traffic_status") // 存储 I/O 限制 (null = 继承套餐) limitsRead String? @map("limits_read") limitsWrite String? @map("limits_write") limitsReadIops Int? @map("limits_read_iops") limitsWriteIops Int? @map("limits_write_iops") // 网络限制 limitsIngress String? @map("limits_ingress") limitsEgress String? @map("limits_egress") // 进程与调度 limitsProcesses Int? @map("limits_processes") limitsCpuPriority Int? @map("limits_cpu_priority") // 启动配置 bootAutostart Boolean? @map("boot_autostart") bootAutostartPriority Int? @map("boot_autostart_priority") bootAutostartDelay Int? @map("boot_autostart_delay") bootHostShutdownTimeout Int? @map("boot_host_shutdown_timeout") // 恢复来源信息 restoredFrom Json? @map("restored_from") // { backupId, backupName, restoredAt } // ========== 计费与封停相关字段 ========== // 到期时间:null 表示永不到期(免费实例),有值表示付费实例的到期时间 expiresAt DateTime? @map("expires_at") // 封停信息 suspendedAt DateTime? @map("suspended_at") // 封停时间 suspendedBy Int? @map("suspended_by") // 封停操作者(null=系统自动,有值=用户ID) suspendReason String? @map("suspend_reason") // 封停原因(expired=到期封停,其他=手动封停原因) // 计费价格(从方案复制,支持后续调整) billingPrice Decimal? @map("billing_price") @db.Decimal(10, 2) // 单周期价格 billingCycle Int? @map("billing_cycle") // 计费周期(月) // 自动续费 autoRenew Boolean @default(false) @map("auto_renew") iconBadgeId String? @map("icon_badge_id") autoRenewAttempts Int @default(0) @map("auto_renew_attempts") // 已尝试次数 lastAutoRenewAttemptAt DateTime? @map("last_auto_renew_attempt_at") // 上次尝试时间 // 到期提醒记录 expiryNotifiedAt DateTime? @map("expiry_notified_at") // 上次发送到期提醒时间 // 乐观锁 version Int @default(0) // 版本号,用于并发控制 // IPv6 重新分配冷却 lastIpv6ReassignAt DateTime? @map("last_ipv6_reassign_at") // 上次重新分配 IPv6 时间(每实例每天限一次) // 方案变更冷却 lastPlanChangeAt DateTime? @map("last_plan_change_at") // 上次变更方案时间(3天冷却期) // 用户自定义显示顺序;相同顺序时按创建时间倒序 displayOrder Int @default(0) @map("display_order") // Cloud-init 检测状态 cloudInitState String? @map("cloud_init_state") cloudInitSource String? @map("cloud_init_source") cloudInitLastCheckedAt DateTime? @map("cloud_init_last_checked_at") cloudInitCompletedAt DateTime? @map("cloud_init_completed_at") cloudInitManualCompletedAt DateTime? @map("cloud_init_manual_completed_at") cloudInitManualCompletedBy Int? @map("cloud_init_manual_completed_by") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") lastSyncedAt DateTime? @map("last_synced_at") // 最后与 Incus 同步时间 user User @relation(fields: [userId], references: [id]) host Host @relation(fields: [hostId], references: [id]) package Package? @relation(fields: [packageId], references: [id], onDelete: SetNull) packagePlan PackagePlan? @relation(fields: [packagePlanId], references: [id], onDelete: SetNull) portMappings PortMapping[] snapshots Snapshot[] backups Backup[] backupPolicy BackupPolicy? snapshotPolicy SnapshotPolicy? trafficSnapshot TrafficSnapshot? dailyTraffic DailyTraffic[] transfers InstanceTransfer[] ipAddresses IpAddress[] ipv6Subnets Ipv6Subnet[] proxySites ProxySite[] instanceTasks InstanceTask[] tickets Ticket[] @relation("InstanceTickets") checkinRecords CheckinRecord[] redeemCodeUsages RedeemCodeUsage[] billingRecords InstanceBillingRecord[] // 计费记录 affBinding AffBinding? // AFF优惠码绑定 resourcePoolLogs ResourcePoolLog[] // 资源池应用记录 destroyRecords UserDestroyRecord[] // 销毁记录 appliedBadgeOwnerships UserBadgeOwnership[] @relation("InstanceAppliedBadgeOwnerships") logs Log[] auditIgnores InstanceAuditIgnore[] auditScans InstanceAuditScan[] auditActions InstanceAuditAction[] @@index([userId]) @@index([hostId]) @@index([status]) @@index([hostId, status]) // 高可用优化:状态同步调度器使用 @@index([expiresAt]) // 计费调度器使用:查找即将到期的实例 @@index([userId, displayOrder, createdAt(sort: Desc)]) @@map("instances") } // IP 地址类型枚举 enum IpType { inet4 inet6 } // IP 地址池模型 (支持多 IP) model IpAddress { id Int @id @default(autoincrement()) address String type IpType @default(inet6) isPrimary Boolean @default(false) @map("is_primary") isCustom Boolean @default(false) @map("is_custom") // 用户自定义 IP device String @default("eth1") // 网卡名: eth0(NAT), eth1(routed IPv6) hostId Int @map("host_id") host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) instanceId Int @map("instance_id") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @map("created_at") @@index([address]) @@index([hostId]) @@index([hostId, address]) @@index([instanceId]) @@map("ip_addresses") } // IPv6 网段分配模型 model Ipv6Subnet { id Int @id @default(autoincrement()) cidr String @unique // 网段 CIDR,如 "2a01:4f9:c012:e7:aaaa::/112" primaryIp String @map("primary_ip") // 网段主 IP (通常是 ::1) device String @default("eth1") instanceId Int @map("instance_id") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @map("created_at") @@index([instanceId]) @@map("ipv6_subnets") } model PortMapping { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") hostId Int @map("host_id") protocol Protocol @default(tcp) publicPort Int @map("public_port") privatePort Int @map("private_port") remark String? createdAt DateTime @default(now()) @map("created_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) host Host @relation(fields: [hostId], references: [id]) @@unique([hostId, protocol, publicPort]) @@index([instanceId]) @@map("port_mappings") } // ==================== 快照/备份相关模型 ==================== model Snapshot { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") incusName String @map("incus_name") name String description String? stateful Boolean @default(false) size Int @default(0) createdAt DateTime @default(now()) @map("created_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@index([instanceId]) @@map("snapshots") } model Backup { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") incusName String @map("incus_name") name String description String? size Int @default(0) status BackupStatus @default(creating) createdAt DateTime @default(now()) @map("created_at") expiresAt DateTime? @map("expires_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@index([instanceId]) @@index([status]) @@map("backups") } model BackupPolicy { id Int @id @default(autoincrement()) instanceId Int @unique @map("instance_id") enabled Boolean @default(false) intervalMinutes Int @default(1440) @map("interval_minutes") // 默认24小时 lastRunAt DateTime? @map("last_run_at") nextRunAt DateTime? @map("next_run_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@index([enabled, nextRunAt]) // 高可用优化:自动策略调度器使用 @@map("backup_policies") } model SnapshotPolicy { id Int @id @default(autoincrement()) instanceId Int @unique @map("instance_id") enabled Boolean @default(false) intervalMinutes Int @default(360) @map("interval_minutes") // 默认6小时 lastRunAt DateTime? @map("last_run_at") nextRunAt DateTime? @map("next_run_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@index([enabled, nextRunAt]) // 高可用优化:自动策略调度器使用 @@map("snapshot_policies") } // ==================== 通知相关模型 ==================== model NotificationChannel { id Int @id @default(autoincrement()) userId Int @map("user_id") type NotificationChannelType name String config Json enabled Boolean @default(true) isGlobal Boolean @default(false) @map("is_global") // 管理员创建的全局渠道,对所有托管用户开放 createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) logs NotificationLog[] packages Package[] @relation("PackageReleaseChannel") // 套餐资源释放通知 @@index([userId]) @@map("notification_channels") } model NotificationLog { id Int @id @default(autoincrement()) channelId Int @map("channel_id") eventType String @map("event_type") message String status NotificationLogStatus error String? createdAt DateTime @default(now()) @map("created_at") channel NotificationChannel @relation(fields: [channelId], references: [id], onDelete: Cascade) @@map("notification_logs") } // ==================== OAuth 相关模型 ==================== model OAuthConfig { id Int @id @default(autoincrement()) provider OAuthProvider @unique clientId String @map("client_id") clientSecret String @map("client_secret") enabled Boolean @default(false) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("oauth_configs") } model UserOAuthBinding { id Int @id @default(autoincrement()) userId Int @map("user_id") provider OAuthProvider providerUserId String @map("provider_user_id") providerUsername String? @map("provider_username") providerEmail String? @map("provider_email") providerAvatar String? @map("provider_avatar") accessToken String? @map("access_token") refreshToken String? @map("refresh_token") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, provider]) @@unique([provider, providerUserId]) @@index([userId]) @@index([provider, providerUserId]) @@map("user_oauth_bindings") } model UserTelegramBinding { id Int @id @default(autoincrement()) userId Int @unique @map("user_id") telegramUserId String @unique @map("telegram_user_id") telegramUsername String? @map("telegram_username") firstName String? @map("first_name") lastName String? @map("last_name") boundAt DateTime @default(now()) @map("bound_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_telegram_bindings") } model TelegramBindToken { id Int @id @default(autoincrement()) userId Int @map("user_id") tokenHash String @unique @map("token_hash") expiresAt DateTime @map("expires_at") usedAt DateTime? @map("used_at") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("telegram_bind_tokens") } // ==================== 帮助文档模型 ==================== model HelpArticle { id Int @id @default(autoincrement()) title String slug String @unique content String category String @default("general") sortOrder Int @default(0) @map("sort_order") published Boolean @default(true) pinned Boolean @default(false) // 是否置顶,置顶文章会显示在用户后台首页 createdBy Int? @map("created_by") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") creator User? @relation("CreatedArticles", fields: [createdBy], references: [id], onDelete: SetNull) @@map("help_articles") } // ==================== 系统配置模型 ==================== model SystemConfig { id Int @id @default(autoincrement()) key String @unique value String type String @default("string") // string, number, boolean, json label String? description String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("system_configs") } // ==================== 流量统计模型 ==================== // 流量快照 (用于增量计算) model TrafficSnapshot { id Int @id @default(autoincrement()) instanceId Int @unique @map("instance_id") rxRaw BigInt @map("rx_raw") // 上次采集的原始值 txRaw BigInt @map("tx_raw") updatedAt DateTime @updatedAt @map("updated_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@map("traffic_snapshots") } // 每日流量聚合 (用于图表) model DailyTraffic { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") date DateTime @db.Date // 只存日期 rxTotal BigInt @map("rx_total") txTotal BigInt @map("tx_total") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@unique([instanceId, date]) @@index([instanceId, date]) @@map("daily_traffic") } // ==================== 远程存储配置模型 ==================== model StorageConfig { id Int @id @default(autoincrement()) userId Int @map("user_id") name String // e.g. "我的群晖 NAS" type StorageType // 连接信息 host String port Int? username String? password String? // ★ 注意:写入前必须 AES 加密,读取时解密 basePath String? @map("base_path") // e.g. "/backups/" // 扩展配置 (预留 S3 bucket/region/accessKey/secretKey 等) extra Json? isDefault Boolean @default(false) @map("is_default") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) uploadTasks BackupUploadTask[] @@index([userId]) @@map("storage_configs") } // ==================== 备份上传任务模型 ==================== model BackupUploadTask { id Int @id @default(autoincrement()) userId Int @map("user_id") instanceId Int @map("instance_id") backupId Int @map("backup_id") hostId Int @map("host_id") storageConfigId Int @map("storage_config_id") status BackupUploadTaskStatus @default(PENDING) progress String? // 当前执行步骤: preparing, uploading, finalizing retryCount Int @default(0) @map("retry_count") // 重试次数 // 上传结果 remoteFileName String? @map("remote_file_name") fileSize BigInt? @map("file_size") error String? createdAt DateTime @default(now()) @map("created_at") startedAt DateTime? @map("started_at") finishedAt DateTime? @map("finished_at") storageConfig StorageConfig @relation(fields: [storageConfigId], references: [id], onDelete: Cascade) @@index([hostId, status]) @@index([userId]) @@index([instanceId]) @@index([storageConfigId, status]) @@map("backup_upload_tasks") } // ==================== 日志模型 ==================== model Log { id Int @id @default(autoincrement()) userId Int? @map("user_id") instanceId Int? @map("instance_id") module String action String content String result String createdAt DateTime @default(now()) @map("created_at") user User? @relation(fields: [userId], references: [id], onDelete: SetNull) instance Instance? @relation(fields: [instanceId], references: [id], onDelete: SetNull) @@index([userId]) @@index([instanceId]) @@index([module]) @@index([createdAt]) @@map("logs") } model InstanceAuditRule { id Int @id @default(autoincrement()) hostId Int? @map("host_id") createdById Int @map("created_by_id") name String description String? severity String @default("medium") category String @default("custom") targetTypes Json @default("[]") @map("target_types") matchType String @default("contains") @map("match_type") pattern String @db.Text caseSensitive Boolean @default(false) @map("case_sensitive") recommendation String? @db.Text enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") host Host? @relation(fields: [hostId], references: [id], onDelete: Cascade) createdBy User @relation("InstanceAuditRuleCreator", fields: [createdById], references: [id], onDelete: Cascade) @@index([hostId, enabled]) @@index([createdById]) @@map("instance_audit_rules") } model InstanceAuditBuiltinRuleOverride { id Int @id @default(autoincrement()) hostId Int @map("host_id") builtinRuleId String @map("builtin_rule_id") createdById Int @map("created_by_id") name String description String? severity String @default("medium") category String @default("custom") targetTypes Json @default("[]") @map("target_types") matchType String @default("contains") @map("match_type") pattern String @db.Text caseSensitive Boolean @default(false) @map("case_sensitive") recommendation String? @db.Text enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) createdBy User @relation("InstanceAuditBuiltinRuleOverrideCreator", fields: [createdById], references: [id], onDelete: Cascade) @@unique([hostId, builtinRuleId]) @@index([hostId, enabled]) @@index([createdById]) @@map("instance_audit_builtin_rule_overrides") } model InstanceAuditIgnore { id Int @id @default(autoincrement()) hostId Int @map("host_id") instanceId Int? @map("instance_id") createdById Int @map("created_by_id") ruleId String? @map("rule_id") targetType String? @map("target_type") matchText String? @map("match_text") scope String @default("instance") reason String? @db.Text enabled Boolean @default(true) expiresAt DateTime? @map("expires_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade) createdBy User @relation("InstanceAuditIgnoreCreator", fields: [createdById], references: [id], onDelete: Cascade) @@index([hostId, enabled]) @@index([instanceId]) @@index([ruleId]) @@map("instance_audit_ignores") } model InstanceAuditScan { id Int @id @default(autoincrement()) hostId Int @map("host_id") instanceId Int @map("instance_id") userId Int @map("user_id") status String @default("success") capability String? riskLevel String @map("risk_level") findingCount Int @default(0) @map("finding_count") ignoredCount Int @default(0) @map("ignored_count") processCount Int @default(0) @map("process_count") connectionCount Int @default(0) @map("connection_count") listeningCount Int @default(0) @map("listening_count") startupItemCount Int @default(0) @map("startup_item_count") findings Json @default("[]") error String? @db.Text createdAt DateTime @default(now()) @map("created_at") host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) user User @relation("InstanceAuditScanUser", fields: [userId], references: [id], onDelete: Cascade) actions InstanceAuditAction[] @@index([hostId, createdAt(sort: Desc)]) @@index([instanceId, createdAt(sort: Desc)]) @@map("instance_audit_scans") } model InstanceAuditAction { id Int @id @default(autoincrement()) scanId Int? @map("scan_id") hostId Int @map("host_id") instanceId Int @map("instance_id") userId Int @map("user_id") actionType String @map("action_type") pid Int? signal String? processCommand String? @map("process_command") @db.Text reason String @db.Text result String stdout String? @db.Text stderr String? @db.Text createdAt DateTime @default(now()) @map("created_at") scan InstanceAuditScan? @relation(fields: [scanId], references: [id], onDelete: SetNull) host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) user User @relation("InstanceAuditActionUser", fields: [userId], references: [id], onDelete: Cascade) @@index([hostId, createdAt(sort: Desc)]) @@index([instanceId, createdAt(sort: Desc)]) @@map("instance_audit_actions") } // ==================== 邮件验证码模型 ==================== model EmailVerificationCode { id Int @id @default(autoincrement()) email String code String expiresAt DateTime @map("expires_at") createdAt DateTime @default(now()) @map("created_at") @@index([email, code]) @@index([expiresAt]) @@map("email_verification_codes") } // ==================== 套餐共享模型 ==================== model PackageShare { id Int @id @default(autoincrement()) packageId Int @map("package_id") // 被共享的套餐 ownerId Int @map("owner_id") // 套餐所有者 sharedToId Int @map("shared_to_id") // 共享给谁(必须是好友) quotaMultiplier Decimal? @map("quota_multiplier") @db.Decimal(3, 1) // 配额倍数限制,如 0.5, 1.0, 1.5, 2.0,null 表示无限制 maxInstances Int? @map("max_instances") // 最多可开实例数,null 表示无限制 createdAt DateTime @default(now()) @map("created_at") package Package @relation(fields: [packageId], references: [id], onDelete: Cascade) owner User @relation("PackageShareOwner", fields: [ownerId], references: [id], onDelete: Cascade) sharedTo User @relation("PackageShareReceiver", fields: [sharedToId], references: [id], onDelete: Cascade) @@unique([packageId, sharedToId]) // 同一套餐不能重复共享给同一人 @@index([sharedToId]) @@index([ownerId]) @@index([packageId]) @@map("package_shares") } // ==================== 实例转移模型 ==================== enum TransferStatus { pending // 等待接收 processing // 处理中(临时锁定状态,防止并发) accepted // 已接收 rejected // 已拒绝 cancelled // 已取消 } model InstanceTransfer { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") fromUserId Int @map("from_user_id") toUserId Int @map("to_user_id") status TransferStatus @default(pending) snapshot Json // 转移时的实例快照信息 remark String? // 转移备注 rejectReason String? @map("reject_reason") fee Decimal? @db.Decimal(10, 2) // 转移手续费(元),拒绝时退还 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 状态变更时自动更新,用于超时检测 acceptedAt DateTime? @map("accepted_at") rejectedAt DateTime? @map("rejected_at") cancelledAt DateTime? @map("cancelled_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) fromUser User @relation("TransferFrom", fields: [fromUserId], references: [id]) toUser User @relation("TransferTo", fields: [toUserId], references: [id]) @@index([fromUserId, status]) @@index([toUserId, status]) @@index([instanceId, status]) @@map("instance_transfers") } // ==================== 敏感操作二次验证模型 ==================== enum OperationType { // 账号相关操作(邮件验证) change_password disable_2fa change_email delete_account // 资源相关操作(通知渠道验证) delete_instance reinstall_instance recreate_instance // 重建实例 transfer_instance delete_snapshot delete_backup } enum VerificationChannel { email telegram discord webhook } model OperationVerification { id Int @id @default(autoincrement()) userId Int @map("user_id") operationType OperationType @map("operation_type") code String // 6位验证码 channel VerificationChannel // 验证渠道 resourceId Int? @map("resource_id") // 资源ID(如实例ID、快照ID等) resourceType String? @map("resource_type") // 资源类型(instance/snapshot/backup) verified Boolean @default(false) verifiedAt DateTime? @map("verified_at") expiresAt DateTime @map("expires_at") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, operationType, code]) @@index([expiresAt]) @@map("operation_verifications") } // ==================== 分布式锁模型 ==================== // LOGIC004: 支持多实例部署的分布式锁 model DistributedLock { id Int @id @default(autoincrement()) lockKey String @unique @map("lock_key") // 锁的唯一标识 ownerId String @map("owner_id") // 锁持有者ID(进程ID+随机字符串) acquiredAt DateTime @map("acquired_at") // 获取时间 expiresAt DateTime @map("expires_at") // 过期时间 @@index([expiresAt]) // 用于清理过期锁 @@map("distributed_locks") } // ==================== 系统镜像模型 ==================== model SystemImage { id Int @id @default(autoincrement()) name String // 显示名称,如 "Ubuntu 24.04 LTS" remoteAlias String @unique @map("remote_alias") // Incus 别名,如 "ubuntu/noble/cloud" osType String @default("Linux") @map("os_type") architecture String @default("x86_64") instanceType String @default("both") @map("instance_type") // 支持的实例类型: container, vm, both icon String // 图标标识,如 "ubuntu" sortOrder Int @default(0) @map("sort_order") hidden Boolean @default(false) // 是否隐藏 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") allowedHosts HostAllowedImage[] @@map("system_images") } model HostAllowedImage { id Int @id @default(autoincrement()) hostId Int @map("host_id") imageId Int @map("image_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) image SystemImage @relation(fields: [imageId], references: [id], onDelete: Cascade) @@unique([hostId, imageId]) @@index([hostId]) @@index([imageId]) @@map("host_allowed_images") } // ==================== 公告/通知历史记录模型 ==================== enum AnnouncementType { system_broadcast // 管理员全站公告 host_broadcast // 宿主机节点公告 admin_message // 管理员私信 host_message // 宿主机所有者私信 } model Announcement { id Int @id @default(autoincrement()) type AnnouncementType // 公告类型 senderId Int @map("sender_id") // 发送者 ID title String // 标题 content String // 内容 recipientCount Int @map("recipient_count") // 发送人数 // 可选关联(根据类型) hostId Int? @map("host_id") // 节点公告时的节点 ID targetUserId Int? @map("target_user_id") // 私信时的目标用户 instanceId Int? @map("instance_id") // 实例私信时的实例 ID createdAt DateTime @default(now()) @map("created_at") sender User @relation("AnnouncementSender", fields: [senderId], references: [id], onDelete: Cascade) host Host? @relation("HostAnnouncements", fields: [hostId], references: [id], onDelete: SetNull) @@index([senderId, createdAt(sort: Desc)]) @@index([hostId, createdAt(sort: Desc)]) @@index([type, createdAt(sort: Desc)]) @@map("announcements") } // ==================== 站内信模型 ==================== model InboxMessage { id Int @id @default(autoincrement()) userId Int @map("user_id") eventType String @map("event_type") // 事件类型,与 notifier.ts 一致 title String // 消息标题 content String // 消息内容 isRead Boolean @default(false) @map("is_read") data Json? // 原始事件数据,用于前端跳转 createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, isRead]) @@index([userId, createdAt(sort: Desc)]) @@map("inbox_messages") } model HostNotificationEmailTask { id Int @id @default(autoincrement()) userId Int @map("user_id") hostId Int @map("host_id") email String username String hostName String @map("host_name") title String content String status HostNotificationEmailTaskStatus @default(PENDING) retryCount Int @default(0) @map("retry_count") lastError String? @map("last_error") scheduledFor DateTime @map("scheduled_for") createdAt DateTime @default(now()) @map("created_at") startedAt DateTime? @map("started_at") finishedAt DateTime? @map("finished_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) @@index([status, scheduledFor]) @@index([userId]) @@index([hostId]) @@index([startedAt]) @@map("host_notification_email_tasks") } // ==================== 反代站点模型 ==================== model ProxySite { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") hostId Int @map("host_id") // 冗余存储,方便查询 domain String // 域名(不允许泛域名) targetPort Int @map("target_port") // 实例内部端口 httpsEnabled Boolean @default(true) @map("https_enabled") // 是否启用 HTTPS(自动申请 Let's Encrypt 证书) remark String? // 站点备注 status ProxySiteStatus @default(pending) enabled Boolean @default(true) error String? // 错误信息 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) @@unique([hostId, domain]) // 同一宿主机下域名唯一 @@index([instanceId]) @@index([hostId]) @@map("proxy_sites") } // ==================== 登录记录模型 ==================== model LoginRecord { id Int @id @default(autoincrement()) userId Int @map("user_id") ip String // 登录 IP country String? // 国家 region String? // 地区/省份 city String? // 城市 isp String? // ISP 服务商 timezone String? // 时区 userAgent String? @map("user_agent") // 用户代理 createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt(sort: Desc)]) @@index([userId, ip]) @@map("login_records") } // ==================== 工单系统模型 ==================== // 工单状态枚举 enum TicketStatus { open // 待处理 in_progress // 处理中 resolved // 已解决 closed // 已关闭 } // 工单优先级枚举 enum TicketPriority { low normal high urgent } // 工单主表 model Ticket { id Int @id @default(autoincrement()) userId Int @map("user_id") // 发起工单的用户 hostId Int? @map("host_id") // 可选:关联的宿主机(无实例时为null,工单直接发给管理员) instanceId Int? @map("instance_id") // 可选:关联的实例 subject String // 工单标题 category String @default("general") // 分类:general/billing/technical/abuse priority TicketPriority @default(normal) status TicketStatus @default(open) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") resolvedAt DateTime? @map("resolved_at") closedAt DateTime? @map("closed_at") user User @relation("TicketCreator", fields: [userId], references: [id], onDelete: Cascade) host Host? @relation("HostTickets", fields: [hostId], references: [id], onDelete: Cascade) instance Instance? @relation("InstanceTickets", fields: [instanceId], references: [id], onDelete: SetNull) messages TicketMessage[] attachments TicketMessageAttachment[] @@index([userId, status]) @@index([hostId, status]) @@index([status, createdAt(sort: Desc)]) @@map("tickets") } // 工单消息表 model TicketMessage { id Int @id @default(autoincrement()) ticketId Int @map("ticket_id") senderId Int @map("sender_id") // 发送者(用户或宿主机所有者) content String // 消息内容 isFromOwner Boolean @default(false) @map("is_from_owner") // 是否来自宿主机所有者 createdAt DateTime @default(now()) @map("created_at") ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) sender User @relation("TicketMessageSender", fields: [senderId], references: [id], onDelete: Cascade) attachments TicketMessageAttachment[] @@index([ticketId, createdAt]) @@map("ticket_messages") } // 工单消息图片附件 model TicketMessageAttachment { id Int @id @default(autoincrement()) ticketId Int @map("ticket_id") messageId Int @map("message_id") uploaderId Int @map("uploader_id") provider String @default("lsky") providerVersion String @default("v1") @map("provider_version") providerFileId String? @map("provider_file_id") filename String originalName String @map("original_name") mimeType String @map("mime_type") sizeBytes Int @map("size_bytes") width Int? height Int? url String thumbnailUrl String? @map("thumbnail_url") createdAt DateTime @default(now()) @map("created_at") ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) message TicketMessage @relation(fields: [messageId], references: [id], onDelete: Cascade) uploader User @relation("TicketAttachmentUploader", fields: [uploaderId], references: [id], onDelete: Cascade) @@index([ticketId, createdAt]) @@index([messageId]) @@index([uploaderId]) @@map("ticket_message_attachments") } // ==================== 签到系统模型 ==================== // 兑换码类型枚举 enum RedeemCodeType { c // CPU r // 内存 (RAM) d // 硬盘 (Disk) t // 流量 (Traffic) p // 积分 (Points) } // 签到记录表 model CheckinRecord { id Int @id @default(autoincrement()) userId Int @map("user_id") // 签到用户(兑换码所有者) redeemCode String @unique @map("redeem_code") // 兑换码 codeType RedeemCodeType @map("code_type") // 兑换码类型 codeValue Int @map("code_value") // 兑换码数值 expiresAt DateTime @map("expires_at") // 过期时间(3小时) usedAt DateTime? @map("used_at") // 使用时间 usedBy Int? @map("used_by") // 使用者ID usedFor Int? @map("used_for") // 使用的实例ID createdAt DateTime @default(now()) @map("created_at") user User @relation("CheckinRecords", fields: [userId], references: [id], onDelete: Cascade) usedByUser User? @relation("RedeemRecords", fields: [usedBy], references: [id], onDelete: SetNull) instance Instance? @relation(fields: [usedFor], references: [id], onDelete: SetNull) @@index([userId]) @@index([usedBy]) @@index([expiresAt]) @@map("checkin_records") } // 用户签到统计表 model CheckinStats { id Int @id @default(autoincrement()) userId Int @unique @map("user_id") lastCheckinDate DateTime? @map("last_checkin_date") // 最后签到日期 lastRedeemDate DateTime? @map("last_redeem_date") // 最后兑换日期(签到码专用) consecutiveOthersUse Int @default(0) @map("consecutive_others_use") // 连续被他人使用天数 selfOnlyMode Boolean @default(false) @map("self_only_mode") // 是否只能自己使用 user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("checkin_stats") } // ==================== 系统兑换码模型 ==================== // 系统兑换码(宿主机所有者生成) model RedeemCode { id Int @id @default(autoincrement()) code String @unique // 兑换码,格式: h-{random18} hostId Int @map("host_id") // 关联的宿主机 createdById Int @map("created_by_id") // 创建者(宿主机所有者) codeType RedeemCodeType @map("code_type") // 资源类型 codeValue Int @map("code_value") // 资源数值 maxUses Int @default(1) @map("max_uses") // 最大使用次数 usedCount Int @default(0) @map("used_count") // 已使用次数 expiresAt DateTime? @map("expires_at") // 过期时间,null=永不过期 enabled Boolean @default(true) // 是否启用 remark String? // 备注 batchId String? @map("batch_id") // 批次ID,同批次码每用户只能用一张 createdAt DateTime @default(now()) @map("created_at") host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) createdBy User @relation("RedeemCodesCreated", fields: [createdById], references: [id], onDelete: Cascade) usages RedeemCodeUsage[] @@index([hostId]) @@index([createdById]) @@index([enabled]) @@index([batchId]) @@map("redeem_codes") } // 系统兑换码使用记录 model RedeemCodeUsage { id Int @id @default(autoincrement()) redeemCodeId Int @map("redeem_code_id") // 兑换码 ID userId Int @map("user_id") // 使用者 instanceId Int @map("instance_id") // 应用到的实例 batchId String? @map("batch_id") // 批次ID(冗余存储,用于唯一约束) usedAt DateTime @default(now()) @map("used_at") redeemCode RedeemCode @relation(fields: [redeemCodeId], references: [id], onDelete: Cascade) user User @relation("RedeemCodeUsages", fields: [userId], references: [id], onDelete: Cascade) instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@unique([userId, batchId]) // 同批次每用户只能使用一次(NULL值不参与约束) @@index([redeemCodeId]) @@index([userId]) @@index([instanceId]) @@map("redeem_code_usages") } // ==================== 用户自定义初始化命令模型 ==================== model CustomInitCommand { id Int @id @default(autoincrement()) userId Int @map("user_id") name String // 模板名称 command String @db.Text // 命令内容(多行文本) distros String // JSON数组,适配的发行版 ["ubuntu","alpine","all"] description String? // 备注说明 enabled Boolean @default(true) // 是否启用 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([userId, enabled]) @@map("custom_init_commands") } model TerminalSavedCommand { id Int @id @default(autoincrement()) userId Int @map("user_id") name String command String @db.Text description String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, updatedAt]) @@map("terminal_saved_commands") } // ==================== 计费系统模型 ==================== // 余额变动日志 model BalanceLog { id Int @id @default(autoincrement()) userId Int @map("user_id") type BalanceLogType amount Decimal @db.Decimal(10, 2) // 正数=增加,负数=减少 balanceBefore Decimal @map("balance_before") @db.Decimal(10, 2) balanceAfter Decimal @map("balance_after") @db.Decimal(10, 2) // 关联信息 orderId String? @map("order_id") // 充值订单号 instanceId Int? @map("instance_id") // 关联实例 remark String? // 备注 createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt(sort: Desc)]) @@index([type]) @@map("balance_logs") } // 支付渠道配置 model PaymentProvider { id Int @id @default(autoincrement()) name String // 显示名称,如"支付宝"、"微信支付" type PaymentProviderType status PaymentProviderStatus @default(disabled) // 通用配置(JSON,根据类型存储不同字段) // 易支付: { pid, key, apiUrl, notifyUrl } // Stripe: { publicKey, secretKey, webhookSecret } config Json @default("{}") // 支付方式支持(JSON数组) // 如 ["alipay", "wxpay", "qqpay"] 或 ["card"] methods Json @default("[]") // 手续费设置 feeRate Decimal @default(0) @map("fee_rate") @db.Decimal(5, 4) // 费率,如 0.006 = 0.6% feeFixed Decimal @default(0) @map("fee_fixed") @db.Decimal(10, 2) // 固定手续费 // 限额设置 minAmount Decimal @default(1) @map("min_amount") @db.Decimal(10, 2) // 最小充值金额 maxAmount Decimal? @map("max_amount") @db.Decimal(10, 2) // 最大充值金额 sortOrder Int @default(0) @map("sort_order") // 排序 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") rechargeRecords RechargeRecord[] @@map("payment_providers") } // 充值记录 model RechargeRecord { id Int @id @default(autoincrement()) userId Int @map("user_id") providerId Int @map("provider_id") // 订单信息 orderNo String @unique @map("order_no") // 系统订单号 tradeNo String? @map("trade_no") // 第三方交易号 // 金额 amount Decimal @db.Decimal(10, 2) // 充值金额 actualAmount Decimal? @map("actual_amount") @db.Decimal(10, 2) // 实际到账金额 fee Decimal @default(0) @db.Decimal(10, 2) // 平台手续费 // 支付方式 paymentMethod String? @map("payment_method") // alipay/wxpay/card 等 status RechargeStatus @default(pending) // 回调信息 callbackData Json? @map("callback_data") // 支付回调原始数据 callbackAt DateTime? @map("callback_at") providerConfigSnapshot String? @map("provider_config_snapshot") // 订单创建时的支付渠道配置快照(加密) paymentDetails Json? @map("payment_details") // 结构化支付详情(币种/网络/txid等) // 失败信息 failReason String? @map("fail_reason") // IP 与设备 ip String? userAgent String? @map("user_agent") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") expiredAt DateTime? @map("expired_at") // 订单过期时间(按支付渠道发票有效期) completedAt DateTime? @map("completed_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) provider PaymentProvider @relation(fields: [providerId], references: [id]) @@index([userId, status]) @@index([status, createdAt]) @@index([orderNo]) @@index([tradeNo]) @@map("recharge_records") } // VIP 等级规则配置 model VipLevelRule { id Int @id @default(autoincrement()) type VipLevelRuleType level Int enabled Boolean @default(true) conditionMode VipLevelConditionMode @default(any) @map("condition_mode") minRecharge Decimal? @map("min_recharge") @db.Decimal(10, 2) minConsume Decimal? @map("min_consume") @db.Decimal(10, 2) minHostingIncome Decimal? @map("min_hosting_income") @db.Decimal(10, 2) minHostingInstances Int? @map("min_hosting_instances") benefits Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([type, level]) @@index([type, enabled]) @@map("vip_level_rules") } model VipBenefitReward { id Int @id @default(autoincrement()) level Int type VipBenefitRewardType title String description String? claimLimit Int @default(1) @map("claim_limit") sortOrder Int @default(0) @map("sort_order") enabled Boolean @default(true) config Json @default("{}") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") claims VipBenefitClaim[] @@index([level, enabled, sortOrder]) @@index([type]) @@map("vip_benefit_rewards") } model VipBenefitClaim { id Int @id @default(autoincrement()) userId Int @map("user_id") rewardId Int @map("reward_id") level Int status VipBenefitClaimStatus claimNo Int @map("claim_no") snapshot Json @default("{}") deliveredAt DateTime? @map("delivered_at") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) reward VipBenefitReward @relation(fields: [rewardId], references: [id], onDelete: Cascade) @@unique([userId, rewardId, claimNo]) @@index([userId, level]) @@index([rewardId]) @@index([status, createdAt]) @@map("vip_benefit_claims") } // 支付回调防重放记录 model PaymentCallback { id Int @id @default(autoincrement()) providerId Int @map("provider_id") // 支付渠道ID orderNo String @map("order_no") // 订单号 tradeNo String? @map("trade_no") // 第三方交易号 callbackIp String? @map("callback_ip") // 回调IP processed Boolean @default(true) // 是否已处理 createdAt DateTime @default(now()) @map("created_at") @@unique([providerId, orderNo, tradeNo]) // 联合唯一索引防重放 @@index([createdAt]) // 用于定期清理 @@map("payment_callbacks") } // 套餐方案(付费实例的核心) model PackagePlan { id Int @id @default(autoincrement()) packageId Int @map("package_id") // 基本信息 name String // 方案名称,如"低配"、"高配"、"标准版" description String? // 方案描述 // ========== 资源配置(预设固定值) ========== cpu Int // CPU 额度 (%),如 50, 100, 200 memory Int // 内存 (MB),如 512, 1024, 2048 disk Int // 磁盘 (MB),如 10240, 20480, 51200 // ========== 配额限制(方案级别独立配置) ========== portLimit Int @map("port_limit") // 端口映射数上限 snapshotLimit Int @map("snapshot_limit") // 快照数上限 backupLimit Int @map("backup_limit") // 备份数上限 siteLimit Int @map("site_limit") // 反代站点数上限 swapSize Int @default(0) @map("swap_size") // SWAP 大小(MB),0 表示不提供 trafficLimit BigInt @map("traffic_limit") // 月流量限额 (Bytes) trafficLimitSpeed String @default("1Mbit") @map("traffic_limit_speed") // 流量超限后的限速值 // ========== 计费配置 ========== price Decimal @db.Decimal(10, 2) // 价格 billingCycle Int @default(1) @map("billing_cycle") // 计费周期(月) setupFee Decimal @default(0) @map("setup_fee") @db.Decimal(10, 2) // 开通费 // ========== 状态与排序 ========== isActive Boolean @default(true) @map("is_active") // 是否启用 isSoldOut Boolean @default(false) @map("is_sold_out") // 是否售罄:显示但不可新购/变更 sortOrder Int @default(0) @map("sort_order") // 排序 // ========== SLA保证(可选) ========== slaGuarantee Decimal? @map("sla_guarantee") @db.Decimal(5, 2) // SLA保证百分比,1-100,两位小数,null=不显示 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关系 package Package @relation(fields: [packageId], references: [id], onDelete: Cascade) instances Instance[] affCodes AffCode[] // AFF优惠码 @@unique([packageId, name]) // 同一套餐下方案名称唯一 @@index([packageId, isActive]) @@map("package_plans") } // 实例计费记录 model InstanceBillingRecord { id Int @id @default(autoincrement()) instanceId Int @map("instance_id") userId Int @map("user_id") type BillingRecordType amount Decimal @db.Decimal(10, 2) // 扣费金额(正数) months Int @default(1) // 购买/续费月数 // 计费周期 periodStart DateTime @map("period_start") // 计费开始时间 periodEnd DateTime @map("period_end") // 计费结束时间 // 关联 balanceLogId Int? @map("balance_log_id") // 关联余额日志 remark String? createdAt DateTime @default(now()) @map("created_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@index([instanceId, createdAt(sort: Desc)]) @@index([userId, createdAt(sort: Desc)]) @@map("instance_billing_records") } // ==================== AFF推荐计划模型 ==================== // AFF优惠码 model AffCode { id Int @id @default(autoincrement()) code String @unique // 优惠码,如 AFF-123-A8K3M9 userId Int @map("user_id") // 创建者 packagePlanId Int? @map("package_plan_id") // 绑定的方案(null表示全局码) discountRate Decimal @default(0.05) @map("discount_rate") @db.Decimal(5, 4) // 折扣率 5% commissionRate Decimal @default(0.05) @map("commission_rate") @db.Decimal(5, 4) // 返利率 5% enabled Boolean @default(true) usedCount Int @default(0) @map("used_count") // 使用次数 totalEarnings Decimal @default(0) @map("total_earnings") @db.Decimal(10, 2) // 总收益 createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) packagePlan PackagePlan? @relation(fields: [packagePlanId], references: [id], onDelete: Cascade) bindings AffBinding[] mailSubscriptionBindings MailSubscriptionAffBinding[] logs AffLog[] @@unique([userId, packagePlanId]) // 每用户每方案仅一个优惠码(包括全局码:packagePlanId=null时每用户最多一个) @@index([userId]) @@index([packagePlanId]) @@map("aff_codes") } // 实例与优惠码永久绑定 model AffBinding { id Int @id @default(autoincrement()) instanceId Int @unique @map("instance_id") // 一个实例只能绑定一个优惠码 affCodeId Int @map("aff_code_id") createdAt DateTime @default(now()) @map("created_at") instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) affCode AffCode @relation(fields: [affCodeId], references: [id], onDelete: Cascade) @@index([affCodeId]) @@map("aff_bindings") } // 邮箱订阅与优惠码永久绑定 model MailSubscriptionAffBinding { id Int @id @default(autoincrement()) mailSubscriptionId Int @unique @map("mail_subscription_id") affCodeId Int @map("aff_code_id") createdAt DateTime @default(now()) @map("created_at") mailSubscription MailSubscription @relation(fields: [mailSubscriptionId], references: [id], onDelete: Cascade) affCode AffCode @relation(fields: [affCodeId], references: [id], onDelete: Cascade) @@index([affCodeId]) @@map("mail_subscription_aff_bindings") } // AFF余额变动日志 model AffLog { id Int @id @default(autoincrement()) userId Int @map("user_id") // 收益用户 affCodeId Int? @map("aff_code_id") // 关联优惠码 instanceId Int? @map("instance_id") // 关联实例 mailSubscriptionId Int? @map("mail_subscription_id") // 关联邮箱订阅 type AffLogType amount Decimal @db.Decimal(10, 2) // 正数=收入,负数=转出 originalAmount Decimal? @map("original_amount") @db.Decimal(10, 2) // 原始订单金额 balanceBefore Decimal @map("balance_before") @db.Decimal(10, 2) balanceAfter Decimal @map("balance_after") @db.Decimal(10, 2) remark String? createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) affCode AffCode? @relation(fields: [affCodeId], references: [id], onDelete: SetNull) mailSubscription MailSubscription? @relation(fields: [mailSubscriptionId], references: [id], onDelete: SetNull) @@index([userId, createdAt(sort: Desc)]) @@index([affCodeId]) @@index([mailSubscriptionId]) @@map("aff_logs") } // AFF余额转化申请 model AffWithdrawal { id Int @id @default(autoincrement()) userId Int @map("user_id") amount Decimal @db.Decimal(10, 2) // 申请转化金额 status AffWithdrawalStatus @default(pending) rejectReason String? @map("reject_reason") reviewedBy Int? @map("reviewed_by") // 审核管理员ID reviewedAt DateTime? @map("reviewed_at") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, status]) @@index([status, createdAt]) @@map("aff_withdrawals") } // ==================== 积分系统模型 ==================== // 积分日志类型 enum PointsLogType { convert // 消费兑换 lottery_win // 抽奖获得 lottery_spend // 抽奖消耗 admin_adjust // 管理员调整 checkin // 签到奖励 badge_draw_spend // 徽章随机抽取消耗 badge_select_spend // 徽章自选消耗 invite_generate // 生成邀请码消耗 vip_benefit // VIP福利领取 } enum UserBadgeSource { draw lottery select admin_grant } enum BadgeApplicationTarget { avatar instance } // 用户积分表 model UserPoints { userId Int @id @map("user_id") // user_id 是主键 points Int @default(0) // 当前积分 totalEarned Int @default(0) @map("total_earned") // 累计获得 totalSpent Int @default(0) @map("total_spent") // 累计消耗 lastConvertedAt DateTime? @map("last_converted_at") // 上次兑换时间 convertedConsume Decimal @default(0) @db.Decimal(10, 2) @map("converted_consume") // 已兑换的消费金额 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_points") } // 积分变动日志 model PointsLog { id Int @id @default(autoincrement()) userId Int @map("user_id") type PointsLogType amount Int // 变动数量(正数为获得,负数为消耗) pointsBefore Int @map("points_before") pointsAfter Int @map("points_after") relatedId Int? @map("related_id") // 关联ID(抽奖记录ID等) remark String? createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt(sort: Desc)]) @@map("points_logs") } model BadgeSeries { id String @id title String nameZh String @map("name_zh") nameEn String? @map("name_en") description String sourceId String? @map("source_id") sourceLabel String? @map("source_label") displayOrder Int @default(0) @map("display_order") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") badges Badge[] @@index([displayOrder]) @@index([isActive]) @@map("badge_series") } model Badge { id String @id name String nameEn String? @map("name_en") fullLabel String @map("full_label") seriesId String @map("series_id") sourceId String? @map("source_id") sourceLabel String? @map("source_label") assetUrl String @map("asset_url") assetUrlDark String? @map("asset_url_dark") assetUrlLight String? @map("asset_url_light") displayOrder Int @default(0) @map("display_order") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") series BadgeSeries @relation(fields: [seriesId], references: [id], onDelete: Restrict) @@index([seriesId, displayOrder]) @@index([isActive]) @@map("badges") } model UserBadgeOwnership { id Int @id @default(autoincrement()) userId Int @map("user_id") badgeId String @map("badge_id") source UserBadgeSource @default(draw) applicationTarget BadgeApplicationTarget? @map("application_target") appliedInstanceId Int? @map("applied_instance_id") appliedAt DateTime? @map("applied_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) appliedInstance Instance? @relation("InstanceAppliedBadgeOwnerships", fields: [appliedInstanceId], references: [id], onDelete: SetNull) @@index([userId, createdAt(sort: Desc)]) @@index([userId, badgeId]) @@index([userId, applicationTarget]) @@index([appliedInstanceId]) @@map("user_badge_ownerships") } // ==================== 抽奖系统模型 ==================== // 奖品类型 enum LotteryPrizeType { nothing // 再接再历 points // 积分奖励 balance // 余额奖励 badge // 随机徽章奖励 instance // 实例奖励(需手动发放) cpu // CPU资源(存入资源池) memory // 内存资源(存入资源池) disk // 硬盘资源(存入资源池) traffic // 流量资源(存入资源池) } // 中奖记录状态 enum LotteryRecordStatus { pending // 待发放(仅实例奖励) delivered // 已发放 claimed // 已领取(用户提交工单后) } // 抽奖活动 model Lottery { id Int @id @default(autoincrement()) name String // 抽奖名称 description String? // 描述 costPoints Int @map("cost_points") // 每次抽奖消耗积分 isActive Boolean @default(true) @map("is_active") // 是否启用 startAt DateTime? @map("start_at") // 开始时间(可选) endAt DateTime? @map("end_at") // 结束时间(可选) totalDraws Int @default(0) @map("total_draws") // 总抽奖次数 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") createdBy Int @map("created_by") // 创建者管理员ID prizes LotteryPrize[] records LotteryRecord[] notificationConfig LotteryNotificationConfig? @@map("lotteries") } // 抽奖奖品 model LotteryPrize { id Int @id @default(autoincrement()) lotteryId Int @map("lottery_id") name String // 奖品名称 type LotteryPrizeType // 奖品类型 value Int @default(0) // 奖品值(积分数/余额分/实例配置ID) probability Decimal @db.Decimal(5, 2) // 中奖概率(百分比,如 30.00 表示 30%) totalQuantity Int? @map("total_quantity") // 总数量(null表示无限) remainQuantity Int? @map("remain_quantity") // 剩余数量 displayOrder Int @default(0) @map("display_order") // 显示顺序 createdAt DateTime @default(now()) @map("created_at") // 实例奖励专用字段 instanceDesc String? @map("instance_desc") // 实例描述(如:1核1G 30天) lottery Lottery @relation(fields: [lotteryId], references: [id], onDelete: Cascade) records LotteryRecord[] @@index([lotteryId]) @@map("lottery_prizes") } // 中奖记录 model LotteryRecord { id Int @id @default(autoincrement()) lotteryId Int @map("lottery_id") prizeId Int @map("prize_id") userId Int @map("user_id") prizeType LotteryPrizeType @map("prize_type") prizeValue Int @map("prize_value") // 中奖时的奖品值(快照) prizeName String @map("prize_name") // 中奖时的奖品名称(快照) status LotteryRecordStatus @default(delivered) pointsSpent Int @map("points_spent") // 消耗的积分 deliveredAt DateTime? @map("delivered_at") // 发放时间 deliveredBy Int? @map("delivered_by") // 发放管理员(实例奖励) ticketId Int? @map("ticket_id") // 关联工单ID(实例奖励) notificationSent Boolean @default(false) @map("notification_sent") createdAt DateTime @default(now()) @map("created_at") lottery Lottery @relation(fields: [lotteryId], references: [id], onDelete: Cascade) prize LotteryPrize @relation(fields: [prizeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt(sort: Desc)]) @@index([lotteryId]) @@index([status]) @@map("lottery_records") } // 抽奖通知配置(管理员为抽奖配置的通知渠道) model LotteryNotificationConfig { id Int @id @default(autoincrement()) lotteryId Int @unique @map("lottery_id") enabled Boolean @default(true) // 通知方式配置(与现有通知渠道类型一致) type String // telegram, discord, webhook config Json // { botToken, chatId } 或 { webhookUrl } 等 // 通知条件 notifyBalance Boolean @default(true) @map("notify_balance") // 余额中奖通知 notifyInstance Boolean @default(true) @map("notify_instance") // 实例中奖通知 // 注意:积分中奖不通知 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") lottery Lottery @relation(fields: [lotteryId], references: [id], onDelete: Cascade) @@map("lottery_notification_configs") } // ==================== 用户资源池系统 ==================== // 资源池操作类型枚举 enum ResourcePoolAction { checkin // 签到获得 redeem // 兑换码兑换 admin_grant // 管理员赠送 system_grant // 系统活动奖励 lottery // 抽奖获得 apply // 应用到实例 system_redeem // 系统h-兑换码兑换 } // 用户资源池 model UserResourcePool { id Int @id @default(autoincrement()) userId Int @unique @map("user_id") cpu Int @default(0) // CPU额度 (%) memory Int @default(0) // 内存额度 (MB) disk Int @default(0) // 硬盘额度 (MB) traffic BigInt @default(0) // 流量额度 (GB) updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_resource_pools") } // 资源池变动记录 model ResourcePoolLog { id Int @id @default(autoincrement()) userId Int @map("user_id") action ResourcePoolAction resourceType RedeemCodeType @map("resource_type") // c/r/d/t amount Int // 正数=获得,负数=消耗 instanceId Int? @map("instance_id") // 应用到的实例 remark String? createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) instance Instance? @relation(fields: [instanceId], references: [id], onDelete: SetNull) @@index([userId]) @@index([userId, action]) @@map("resource_pool_logs") } // 用户实例销毁记录 model UserDestroyRecord { id Int @id @default(autoincrement()) userId Int @map("user_id") hostId Int @map("host_id") // 节点ID(用于90天节点冷却期) instanceId Int @map("instance_id") // 被销毁的实例ID instanceName String @map("instance_name") // 实例名称(备份,因实例已删除) destroyedAt DateTime @default(now()) @map("destroyed_at") refundAmount Decimal @db.Decimal(10, 2) @map("refund_amount") // 实际退款金额 feeAmount Decimal @db.Decimal(10, 2) @map("fee_amount") // 手续费金额 isFirstTime Boolean @map("is_first_time") // 是否首次销毁(首次免手续费) user User @relation(fields: [userId], references: [id], onDelete: Cascade) host Host @relation(fields: [hostId], references: [id], onDelete: Cascade) instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) @@unique([instanceId]) @@index([userId]) @@index([userId, hostId]) @@map("user_destroy_records") } // ==================== 托管余额系统模型 ==================== enum HostingBalanceType { income // 收入(用户购买/续费) unfreeze // 解冻 withdraw // 提现 deduction // 扣除(用户销毁托管实例) } // 托管收支操作类型(用于区分具体操作) enum HostingActionType { purchase // 用户开通实例 renew // 用户续费实例 upgrade // 用户升级方案 destroy // 用户销毁实例 unfreeze // 自动解冻 withdraw // 提现 admin_adjust // 管理员调整 } enum WithdrawalStatus { pending // 待审核 approved // 已通过 rejected // 已拒绝 completed // 已完成 } enum WithdrawalTarget { balance // 提现到面板余额 usdt // 提现到USDT } // ==================== 域名邮箱模块 ==================== // 域名验证状态 enum MailDomainStatus { pending // 等待 DNS 验证 verified // 已验证 suspended // 已暂停 } // 邮箱订阅状态 enum MailSubscriptionStatus { active // 正常 expired // 已过期 suspended // 已暂停 } // 计费周期 enum MailBillingCycle { monthly // 月付 yearly // 年付 } // 邮箱源(数据中心) model MailSource { id Int @id @default(autoincrement()) name String // 名称,如"美国拉斯维加斯" code String @unique // 地区代码,如"us" apiUrl String @map("api_url") // CraneMail Reseller API 地址 apiKey String @map("api_key") // CraneMail Reseller API Key smarterMailUrl String @map("smartermail_url") // SmarterMail API 地址 enabled Boolean @default(true) sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") plans MailPlan[] subscriptions MailSubscription[] domains MailDomain[] @@map("mail_sources") } // 套餐方案 model MailPlan { id Int @id @default(autoincrement()) sourceId Int @map("source_id") name String // 方案名称,如"基础版" description String? // 方案描述 domainLimit Int @map("domain_limit") // 主域名数量限制 diskLimitGb Int @map("disk_limit_gb") // 存储空间 GB billingCycle MailBillingCycle @map("billing_cycle") // 计费周期 price Decimal @db.Decimal(10, 2) // 价格 enabled Boolean @default(true) sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") source MailSource @relation(fields: [sourceId], references: [id], onDelete: Cascade) subscriptions MailSubscription[] @@index([sourceId, enabled]) @@map("mail_plans") } // 用户订阅 model MailSubscription { id Int @id @default(autoincrement()) userId Int @map("user_id") sourceId Int @map("source_id") planId Int @map("plan_id") status MailSubscriptionStatus @default(active) domainLimit Int @map("domain_limit") // 冗余存储购买时的限制 diskLimitGb Int @map("disk_limit_gb") // 冗余存储购买时的限制 expiresAt DateTime @map("expires_at") autoRenew Boolean @default(false) @map("auto_renew") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) source MailSource @relation(fields: [sourceId], references: [id], onDelete: Cascade) plan MailPlan @relation(fields: [planId], references: [id], onDelete: Cascade) domains MailDomain[] affBinding MailSubscriptionAffBinding? affLogs AffLog[] @@index([userId, status]) @@index([expiresAt]) @@map("mail_subscriptions") } // 域名 model MailDomain { id Int @id @default(autoincrement()) subscriptionId Int @map("subscription_id") sourceId Int @map("source_id") domain String // 域名 status MailDomainStatus @default(pending) adminUsername String? @map("admin_username") // CraneMail 返回的管理员账号 adminPassword String? @map("admin_password") // CraneMail 返回的管理员密码(加密存储) diskUsedMb Int @default(0) @map("disk_used_mb") // 已用空间 MB verifiedAt DateTime? @map("verified_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") subscription MailSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) source MailSource @relation(fields: [sourceId], references: [id], onDelete: Cascade) accounts MailAccount[] @@unique([domain, sourceId]) @@index([subscriptionId]) @@map("mail_domains") } // 邮箱账户 model MailAccount { id Int @id @default(autoincrement()) domainId Int @map("domain_id") email String // 完整邮箱地址 username String // 用户名部分 (@ 前面) displayName String? @map("display_name") // 显示名称 diskLimitMb Int @default(2048) @map("disk_limit_mb") // 邮箱容量 MB diskUsedMb Int @default(0) @map("disk_used_mb") // 已用空间 MB isAdmin Boolean @default(false) @map("is_admin") // 是否为域管理员 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") domain MailDomain @relation(fields: [domainId], references: [id], onDelete: Cascade) @@unique([domainId, username]) @@index([domainId]) @@map("mail_accounts") } // 托管余额变动日志 model HostingBalanceLog { id Int @id @default(autoincrement()) userId Int @map("user_id") type HostingBalanceType actionType HostingActionType? @map("action_type") // 具体操作类型(开通/续费/升级/销毁等) amount Decimal @db.Decimal(10, 2) // 金额(正数) frozen Boolean @default(true) // 是否冻结状态 unfreezeAt DateTime? @map("unfreeze_at") // 解冻时间 relatedId Int? @map("related_id") // 关联ID(实例ID/提现ID) remark String? // Snapshot fields for data persistence snapshotBuyerName String? @map("snapshot_buyer_name") snapshotBuyerEmail String? @map("snapshot_buyer_email") snapshotBuyerAvatar String? @map("snapshot_buyer_avatar") snapshotInstanceName String? @map("snapshot_instance_name") snapshotHostName String? @map("snapshot_host_name") snapshotPackageName String? @map("snapshot_package_name") snapshotPlanName String? @map("snapshot_plan_name") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, frozen]) @@index([unfreezeAt]) @@map("hosting_balance_logs") } // 托管余额提现记录 model HostingWithdrawal { id Int @id @default(autoincrement()) userId Int @map("user_id") amount Decimal @db.Decimal(10, 2) // 提现金额 feeRate Decimal @map("fee_rate") @db.Decimal(5, 4) // 手续费率(如 0.1000 表示 10%) feeAmount Decimal @map("fee_amount") @db.Decimal(10, 2) // 手续费金额 actualAmount Decimal @map("actual_amount") @db.Decimal(10, 2) // 实际到账金额 target WithdrawalTarget usdtAddress String? @map("usdt_address") // USDT地址(仅USDT提现) status WithdrawalStatus @default(pending) rejectReason String? @map("reject_reason") processedAt DateTime? @map("processed_at") processedBy Int? @map("processed_by") // 处理的管理员ID createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, status]) @@map("hosting_withdrawals") } // 托管用户拉黑关系 model HostingUserBlock { id Int @id @default(autoincrement()) blockerId Int @map("blocker_id") // 托管用户 blockedUserId Int @map("blocked_user_id") // 被拉黑用户 remark String? createdAt DateTime @default(now()) @map("created_at") blocker User @relation("HostingBlocker", fields: [blockerId], references: [id], onDelete: Cascade) blockedUser User @relation("HostingBlockedUser", fields: [blockedUserId], references: [id], onDelete: Cascade) @@unique([blockerId, blockedUserId]) @@index([blockedUserId]) @@index([blockerId]) @@map("hosting_user_blocks") } // 托管专区机主 model HostingZone { id Int @id @default(autoincrement()) name String ownerId Int @unique @map("owner_id") logoUrl String @map("logo_url") @db.Text sortOrder Int @default(0) @map("sort_order") active Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") owner User @relation("HostingZoneOwner", fields: [ownerId], references: [id], onDelete: Cascade) @@index([active, sortOrder]) @@map("hosting_zones") }