Files
2026-07-30 12:23:05 +08:00

385 lines
18 KiB
Go

package model
import "time"
// 账户类别取值。
const (
AccountTypePaid = "paid"
AccountTypeTrial = "trial"
AccountTypeFree = "free"
AccountTypeUnknown = "unknown"
)
// 测活状态取值;suspended 来自账户能力接口的云端暂停标记(区别于失联)。
const (
AliveStatusAlive = "alive"
AliveStatusDead = "dead"
AliveStatusSuspended = "suspended"
AliveStatusUnknown = "unknown"
)
// Setting 是系统级键值配置;敏感值(如 telegram_bot_token)以 AES-GCM 密文存储。
type Setting struct {
Key string `gorm:"primaryKey;size:64" json:"key"`
Value string `gorm:"type:text" json:"value"`
UpdatedAt time.Time `json:"updatedAt"`
}
// Proxy 是出站代理配置(socks5 / http / https),供租户 SDK 调用按关联走代理;
// 密码 AES-GCM 加密存储,任何响应不回明文。
type Proxy struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;size:64" json:"name"`
Type string `gorm:"size:16" json:"type"`
Host string `gorm:"size:256" json:"host"`
Port int `json:"port"`
Username string `gorm:"size:128" json:"username"`
PasswordEnc string `json:"-"`
// Country / City 为经代理出口实测的地理位置;GeoAt 为最近一次探测时间,nil 表示尚未探测
Country string `gorm:"size:64" json:"country"`
City string `gorm:"size:64" json:"city"`
GeoAt *time.Time `json:"geoAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// SystemLog 是面板自身的操作审计(区别于 OCI 云端审计事件);
// 只存方法、路径等元数据,绝不存请求体,避免私钥 / 口令入库。
type SystemLog struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;index" json:"username"`
Method string `gorm:"size:8" json:"method"`
Path string `gorm:"size:256" json:"path"`
Status int `json:"status"`
DurationMs int64 `json:"durationMs"`
ClientIP string `gorm:"size:64" json:"clientIp"`
UserAgent string `gorm:"size:256" json:"userAgent"`
// ErrMsg 仅失败请求(>=400)存响应的 error 文案,便于免翻服务端日志定位原因
ErrMsg string `gorm:"size:256" json:"errMsg"`
CreatedAt time.Time `gorm:"index" json:"createdAt"`
}
// User 是可登录面板的账号,密码只存 bcrypt 哈希。
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;size:64" json:"username"`
PasswordHash string `json:"-"`
// TOTP 共享密钥 AES-GCM 密文;空串表示未启用两步验证
TotpSecretEnc string `json:"-"`
// TokenVersion 是 JWT 版本号:凭据/TOTP/外部身份/登录策略变更或撤销会话时
// 原子递增,旧版本令牌随即失效(存量令牌无 ver 视为 0,与零值兼容)
TokenVersion uint `json:"-"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// UserIdentity 是管理员绑定的外部登录身份(OIDC / GitHub);
// 绑定动作只发生在已登录会话内,未绑定的外部身份一律拒绝登录。
type UserIdentity struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index" json:"-"`
Provider string `gorm:"size:16;uniqueIndex:idx_identity_provider_subject" json:"provider"`
// OIDC sub / GitHub 数字 id;不回给前端
Subject string `gorm:"size:255;uniqueIndex:idx_identity_provider_subject" json:"-"`
// 展示名:邮箱 / GitHub login
Display string `gorm:"size:255" json:"display"`
CreatedAt time.Time `json:"createdAt"`
}
// UserPasskey 是账号绑定的 WebAuthn 通行密钥;公钥非敏感,凭据整段 JSON 明文落库。
type UserPasskey struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index" json:"-"`
Name string `gorm:"size:64" json:"name"`
// 凭据 ID(base64url)原文:规范允许原始 ID 达 1023 字节、编码后逾千字符,
// 存 text 不设索引;唯一约束与登录反查走下方定长哈希列
CredentialID string `gorm:"type:text" json:"-"`
// 凭据 ID 的 SHA-256 hex(定长 64):唯一索引防同一验证器重复注册
CredentialIDHash string `gorm:"size:64;uniqueIndex" json:"-"`
// 注册时的 WebAuthn origin(scheme://host):面板地址变更后按它判定
// 该凭据是否仍可登录(RP ID / origin 绑定注册时域名)
Origin string `gorm:"size:255" json:"-"`
// webauthn.Credential 完整 JSON(公钥/signCount/flags);attestation 可上 KB,
// 显式 text 防 MySQL DefaultStringSize 截断;校验后 signCount 有更新,整段重写
Credential string `gorm:"type:text" json:"-"`
CreatedAt time.Time `json:"createdAt"`
LastUsedAt *time.Time `json:"lastUsedAt"`
}
// UserSession 是一次已签发登录会话(JWT)的落地记录;
// 「活跃」判定 = 未撤销 && 未过期 && token_ver == 账号当前版本。
// 升级前签发的存量令牌无对应行,校验时放行(兼容),仅不可见/不可定点撤销。
type UserSession struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index" json:"-"`
// JWT jti;敏感变更换发时同行更新(会话接续),不产生新条目
TokenID string `gorm:"size:64;uniqueIndex" json:"-"`
TokenVer uint `json:"-"`
// 登录方式:password / oidc / github / passkey / wallet;接续换发保留原值
Method string `gorm:"size:16" json:"method"`
ClientIP string `gorm:"size:64" json:"clientIp"`
UserAgent string `gorm:"size:256" json:"userAgent"`
CreatedAt time.Time `json:"createdAt"`
LastSeenAt time.Time `json:"lastSeenAt"`
ExpiresAt time.Time `json:"expiresAt"`
RevokedAt *time.Time `json:"-"`
}
// 任务类型取值。
const (
TaskTypeHealthCheck = "health_check" // 定时测活
TaskTypeSnatch = "snatch" // 抢机:反复尝试创建实例直到成功
TaskTypeCost = "cost" // 每日成本同步:拉 Usage API 写本地快照
TaskTypeAiProbe = "ai_probe" // AI 渠道周期探测:随渠道数量自动创建/激活/暂停
)
// 任务状态取值。
const (
TaskStatusActive = "active" // 按 cron 调度中
TaskStatusPaused = "paused" // 手动暂停
TaskStatusSucceeded = "succeeded" // 抢机成功后自动停止
TaskStatusFailed = "failed" // 熔断停止(如抢机连续鉴权失败)
)
// Task 是一个按 cron 表达式周期执行的后台任务。
// Payload 按任务类型存 JSON:测活为 {"ociConfigIds":[...]}(空为全部),
// 抢机为 {"ociConfigId":1,"instance":{...},"count":1}。
type Task struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:128" json:"name"`
Type string `gorm:"size:32;index" json:"type"`
CronExpr string `gorm:"size:64" json:"cronExpr"`
Payload string `gorm:"type:text" json:"payload"`
Status string `gorm:"size:16;index" json:"status"`
LastRunAt *time.Time `json:"lastRunAt"`
LastError string `gorm:"type:text" json:"lastError"`
RunCount int `json:"runCount"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// TaskLog 是任务的一次执行记录。
type TaskLog struct {
ID uint `gorm:"primaryKey" json:"id"`
TaskID uint `gorm:"index" json:"taskId"`
Success bool `json:"success"`
Message string `gorm:"type:text" json:"message"`
DurationMs int64 `json:"durationMs"`
CreatedAt time.Time `json:"createdAt"`
}
// CheckSnapshot 是测活任务为每个配置留下的最新资源快照(每配置一行,覆盖更新)。
// InstanceCount 只统计配置默认区域,用于总览 KPI 与覆盖度标注。
type CheckSnapshot struct {
ID uint `gorm:"primaryKey" json:"id"`
OciConfigID uint `gorm:"uniqueIndex" json:"ociConfigId"`
AliveStatus string `gorm:"size:16" json:"aliveStatus"`
InstanceCount int `json:"instanceCount"`
CheckedAt time.Time `json:"checkedAt"`
}
// CostSnapshot 是成本任务写入的每配置每日成本(UTC 日界,重复同步覆盖更新)。
type CostSnapshot struct {
ID uint `gorm:"primaryKey" json:"id"`
OciConfigID uint `gorm:"uniqueIndex:idx_cost_cfg_day" json:"ociConfigId"`
Day string `gorm:"size:10;uniqueIndex:idx_cost_cfg_day" json:"day"`
Amount float64 `json:"amount"`
Currency string `gorm:"size:8" json:"currency"`
SyncedAt time.Time `json:"syncedAt"`
}
// OciConfig 保存一份 OCI API Key 配置及其最近一次云端快照。
// 私钥和口令只以 AES-GCM 密文落库,且不出现在 JSON 响应中。
type OciConfig struct {
ID uint `gorm:"primaryKey" json:"id"`
Alias string `gorm:"uniqueIndex;size:64" json:"alias"`
// Group 是面板内的自定义分组标签,空串表示未分组;
// 列名避开 SQL 保留字 GROUP
Group string `gorm:"column:group_name;size:64;index" json:"group"`
// ProxyID 关联出站代理,nil 表示直连;该租户全部 SDK 调用经此代理
ProxyID *uint `gorm:"index" json:"proxyId"`
UserOCID string `json:"userOcid"`
TenancyOCID string `json:"tenancyOcid"`
Fingerprint string `json:"fingerprint"`
Region string `json:"region"`
// 私钥 PEM 密文约 4KB,必须 text;passphrase 密文同档处理
PrivateKeyEnc string `gorm:"type:text" json:"-"`
PassphraseEnc string `gorm:"type:text" json:"-"`
TenancyName string `json:"tenancyName"`
HomeRegionKey string `json:"homeRegionKey"`
AccountType string `json:"accountType"`
SubscriptionID string `json:"subscriptionId"`
PaymentModel string `json:"paymentModel"`
SubscriptionTier string `json:"subscriptionTier"`
PromotionStatus string `json:"promotionStatus"`
PromotionAmount float32 `json:"promotionAmount"`
PromotionExpires *time.Time `json:"promotionExpires"`
AliveStatus string `json:"aliveStatus"`
LastError string `gorm:"type:text" json:"lastError"`
LastVerifiedAt *time.Time `json:"lastVerifiedAt"`
// 多区域 / 多区间支持:开启后订阅区域与 compartment 列表入库缓存,
// 供筛选器免实时调 OCI;未开启的租户筛选器锁定主区域 / 根区间。
MultiRegion bool `json:"multiRegion"`
MultiCompartment bool `json:"multiCompartment"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// LogEvent 是 OCI 日志回传事件(方案A:Connector Hub → Notifications → HTTPS 订阅);
// Payload 存 ONS 消息原文(链路单条 ≤128KB,面板另设 256KB 防御上限,超限截断并标记)。
type LogEvent struct {
ID uint `gorm:"primaryKey" json:"id"`
OciConfigID uint `gorm:"index" json:"ociConfigId"` // 经 secret 反查归属租户
MessageID string `gorm:"uniqueIndex;size:128" json:"messageId"` // 幂等键(X-OCI-NS-MessageId)
EventType string `gorm:"size:128;index" json:"eventType"` // 异步解析回填,如 com.oraclecloud.ComputeApi.TerminateInstance
Source string `gorm:"size:64" json:"source"`
SourceIP string `gorm:"size:64" json:"sourceIp"` // 异步解析回填,Audit 事件的发起方 IP
EventTime *time.Time `gorm:"index" json:"eventTime"`
// 防御上限 256KB 超 MySQL TEXT(64KB),size 上探一档 → MEDIUMTEXT;PG/SQLite 仍为 text
Payload string `gorm:"size:16777216" json:"payload"`
Truncated bool `json:"truncated"`
Processed bool `gorm:"index" json:"processed"`
ReceivedAt time.Time `json:"receivedAt"`
}
// RegionCache 是开启多区域支持的配置缓存的订阅区域(每配置多行,整组覆盖)。
// Status 非 READY(新订阅进行中)时读取接口会实时刷新,直到全部 READY。
type RegionCache struct {
ID uint `gorm:"primaryKey" json:"-"`
OciConfigID uint `gorm:"index;uniqueIndex:idx_region_cache" json:"-"`
Key string `gorm:"size:8;uniqueIndex:idx_region_cache" json:"key"`
Name string `gorm:"size:64" json:"name"`
Status string `gorm:"size:16" json:"status"`
IsHomeRegion bool `json:"isHomeRegion"`
UpdatedAt time.Time `json:"updatedAt"`
}
// CompartmentCache 是开启多区间支持的配置缓存的 compartment(每配置多行,整组覆盖)。
type CompartmentCache struct {
ID uint `gorm:"primaryKey" json:"-"`
OciConfigID uint `gorm:"index;uniqueIndex:idx_comp_cache" json:"-"`
OCID string `gorm:"size:128;uniqueIndex:idx_comp_cache" json:"id"`
Name string `gorm:"size:128" json:"name"`
ParentOCID string `gorm:"size:128" json:"parentId"`
State string `gorm:"size:16" json:"lifecycleState"`
UpdatedAt time.Time `json:"updatedAt"`
}
// AiKey 是 AI 网关的访问密钥;明文不落库,仅存 SHA-256 与尾 4 位。
// Group 非空时该密钥只在同分组渠道内负载均衡(列名避开 SQL 保留字 group)。
type AiKey struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;size:64" json:"name"`
KeyHash string `gorm:"uniqueIndex;size:64" json:"-"`
Tail string `gorm:"size:4" json:"tail"`
Group string `gorm:"column:key_group;size:32" json:"group"`
// Models 是模型白名单(精确匹配模型名);空 = 不限,非空时网关仅放行列表内模型
Models []string `gorm:"serializer:json;type:text" json:"models"`
Enabled bool `json:"enabled"`
LastUsedAt *time.Time `json:"lastUsedAt"`
// ContentLogUntil 是内容日志开启截止时间;nil = 永关(默认,红线),
// 开启必须限时,到期自动停写
ContentLogUntil *time.Time `json:"contentLogUntil"`
CreatedAt time.Time `json:"createdAt"`
}
// AiChannel 是号池渠道 = 租户配置 × 区域;Priority 数字小优先级高,组内按 Weight 加权随机。
// Group 是分组名(列名避开 SQL 保留字 group),空串归属默认分组。
type AiChannel struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:96" json:"name"`
OciConfigID uint `gorm:"uniqueIndex:idx_ai_channel" json:"ociConfigId"`
Region string `gorm:"size:32;uniqueIndex:idx_ai_channel" json:"region"`
Group string `gorm:"column:channel_group;size:32;index" json:"group"`
Enabled bool `json:"enabled"`
Priority int `json:"priority"`
Weight int `json:"weight"`
// FailCount 连续失败计数;达阈值后 DisabledUntil 指数退避熔断,成功/探测通过复位
FailCount int `json:"failCount"`
DisabledUntil *time.Time `json:"disabledUntil"`
LastProbeAt *time.Time `json:"lastProbeAt"`
ProbeStatus string `gorm:"size:16" json:"probeStatus"` // ok / no_service / no_quota / error
ProbeError string `gorm:"size:512" json:"probeError"`
// ProbeModel 是用户测试通过后固定的探测验证模型名;探测时置于候选首位
ProbeModel string `gorm:"size:96" json:"probeModel"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// ModelCount 是渠道模型缓存计数,列表查询回填,不落库
ModelCount int64 `gorm:"-" json:"modelCount"`
}
// AiModelCache 是渠道区域的可用模型缓存(整渠道覆盖式同步)。
// Capability 为 CHAT / EMBEDDING / RERANK / TTS;存量空串视为 CHAT(加列前只同步对话模型)。
type AiModelCache struct {
ID uint `gorm:"primaryKey" json:"-"`
ChannelID uint `gorm:"index" json:"channelId"`
ModelOcid string `gorm:"size:160" json:"modelOcid"`
Name string `gorm:"size:96;index" json:"name"`
Vendor string `gorm:"size:32" json:"vendor"`
Capability string `gorm:"size:16" json:"capability"`
SyncedAt time.Time `json:"syncedAt"`
// DeprecatedAt 是 OCI 宣布的模型弃用时间;nil 表示未宣布。
// 弃用后仍可调用,直到 RetiredAt(按需推理退役,同步层已剔除过期项)。
DeprecatedAt *time.Time `json:"deprecatedAt"`
RetiredAt *time.Time `json:"retiredAt"`
}
// AiModelBlacklist 是全局模型黑名单(按模型名):加入即从全部渠道缓存删除该模型,
// 同步与探测过滤不再入库;适用于微调基座等实际不可按需调用的模型,由用户手动维护。
type AiModelBlacklist struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:96;uniqueIndex" json:"name"`
CreatedAt time.Time `json:"createdAt"`
}
// AiCallLog 是网关调用日志:仅元数据与 token 用量,绝不记录 prompt / 响应正文。
type AiCallLog struct {
ID uint `gorm:"primaryKey" json:"id"`
KeyID uint `json:"keyId"`
KeyName string `gorm:"size:64" json:"keyName"`
Endpoint string `gorm:"size:16" json:"endpoint"` // openai / anthropic
Model string `gorm:"size:96" json:"model"`
ChannelID uint `json:"channelId"`
ChannelName string `gorm:"size:96" json:"channelName"`
Stream bool `json:"stream"`
Status int `json:"status"`
// ClientIP 是网关调用方来源 IP(尊重「真实 IP 头」安全设置,与系统日志同源)
ClientIP string `gorm:"size:64" json:"clientIp"`
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
TotalTokens int `json:"totalTokens"`
// CachedTokens 是输入中缓存命中(读取)的部分;OCI 隐式缓存无写入计数
CachedTokens int `json:"cachedTokens"`
LatencyMs int64 `json:"latencyMs"`
Retries int `json:"retries"`
ErrMsg string `gorm:"size:512" json:"errMsg"`
CreatedAt time.Time `gorm:"index" json:"createdAt"`
}
// AiContentLog 是网关调用正文抽样——「不落正文」红线的唯一显式例外:
// 仅当调用密钥显式开启内容日志且未过期时写入,到期自动停写;
// 请求正文与非流式响应正文各截断至 64KB,流式响应不记录;保留 7 天 / 1 万行。
type AiContentLog struct {
ID uint `gorm:"primaryKey" json:"id"`
// CallLogID 关联同次调用的 AiCallLog,调用日志详情据此反查正文
CallLogID uint `gorm:"index" json:"callLogId"`
KeyID uint `gorm:"index" json:"keyId"`
KeyName string `gorm:"size:64" json:"keyName"`
Endpoint string `gorm:"size:16" json:"endpoint"`
Model string `gorm:"size:96" json:"model"`
Stream bool `json:"stream"`
// RequestBody / ResponseBody 是截断后的正文 JSON;
// 64KB 截断恰在 MySQL TEXT 上限(65535B)边界,size 上探一档 → MEDIUMTEXT
RequestBody string `gorm:"size:16777216" json:"requestBody"`
ResponseBody string `gorm:"size:16777216" json:"responseBody"`
CreatedAt time.Time `gorm:"index" json:"createdAt"`
}