Documentation
¶
Index ¶
- Constants
- func DefaultDurationForPeriod(period string) int
- type Admin
- type Announcement
- type BalanceTransaction
- type EmailVerification
- type InviteCode
- type Node
- type Order
- type Plan
- type PlanPrice
- type PlanPriceEntry
- type PlanPrices
- type RedemptionCode
- type RedemptionRecord
- type RefreshToken
- type SystemConfig
- type Ticket
- type TicketMessage
- type TicketReadState
- type TrafficGrant
- type TrafficHourlyStat
- type TrafficPackage
- type User
- type UserNode
- type UserNodeTraffic
Constants ¶
const ( // BalanceReasonPlanChangeRefund credits the remaining (unamortized) value of // a plan the user is leaving when they switch plans (upgrade or downgrade). BalanceReasonPlanChangeRefund = "plan_change_refund" // BalanceReasonPurchase debits the wallet to pay for an order (plan, // traffic package, or reset). BalanceReasonPurchase = "purchase" // BalanceReasonAdminAdjust is a manual credit/debit applied by an admin. BalanceReasonAdminAdjust = "admin_adjust" // BalanceReasonRefund credits the wallet when an order is refunded. BalanceReasonRefund = "refund" )
Balance transaction reasons. These classify every ledger entry so the wallet history is auditable.
const ( BalanceTypeCredit = "credit" BalanceTypeDebit = "debit" )
Balance transaction types.
const ( OrderStatusPending = "pending" OrderStatusPaid = "paid" OrderStatusClosed = "closed" // OrderPlatform* identifies which payment gateway an order belongs to. // "alipay" is the implemented gateway; "manual" is set when an admin marks // an order paid by hand (no real gateway). Future platforms (wechat, // stripe, ...) are added here and wired into the payment Registry. OrderPlatformAlipay = "alipay" OrderPlatformWechat = "wechat" OrderPlatformStripe = "stripe" OrderPlatformPaypal = "paypal" OrderPlatformApple = "apple" OrderPlatformManual = "manual" // OrderPlatformBalance is set when an order is paid entirely or partly from // the user's account balance (wallet) rather than a payment gateway. OrderPlatformBalance = "balance" // OrderKindPlan is a recurring subscription purchase (applies a plan's // level + quota + duration to the user). OrderKindPlan = "plan" // OrderKindTraffic is a one-time traffic add-on purchase. OrderKindTraffic = "traffic" )
const ( PlanPeriodMonth = "month" // 30 days PlanPeriodQuarter = "quarter" // 90 days PlanPeriodHalfYear = "half_year" // 180 days PlanPeriodYear = "year" // 365 days PlanPeriodMonthDays = 30 PlanPeriodQuarterDays = 90 PlanPeriodHalfYearDays = 180 PlanPeriodYearDays = 365 )
Billing periods offered for a plan. The integer suffix maps to DurationDays.
const ( // RedeemTypeTraffic adds QuotaBytes to the user's quota (opts the user // out of the global monthly reset so the grant is not wiped). RedeemTypeTraffic = "traffic" // RedeemTypeDuration extends the user's ExpireAt by DurationDays (and // re-enables the user if currently disabled). RedeemTypeDuration = "duration" // RedeemTypePlan applies the plan identified by PlanID as a free // subscription (sets quota/level/expiry like a paid plan order). RedeemTypePlan = "plan" )
Redemption benefit types. A RedemptionCode grants the benefit selected by Type when an authenticated user redeems its Code.
const ( TicketStatusOpen = "open" TicketStatusInProgress = "in_progress" TicketStatusResolved = "resolved" TicketStatusClosed = "closed" TicketPriorityLow = "low" TicketPriorityNormal = "normal" TicketPriorityHigh = "high" TicketPriorityUrgent = "urgent" TicketSenderUser = "user" TicketSenderAdmin = "admin" // Ticket notify-method values: the ticket owner's preferred channel for // being notified of admin replies / status changes on THIS ticket. TicketNotifyNone = "none" TicketNotifyEmail = "email" TicketNotifyTelegram = "telegram" )
const ( GrantSourceTrafficPackage = "traffic_package" GrantSourceRedemption = "redemption" )
Traffic grant sources.
const NodeOnlineWindow = 5 * time.Minute
NodeOnlineWindow is how recently a node must have polled (LastSeenAt) to be considered online. Shared by stats.OverviewStats.NodeOnline and the per-node Online() computation.
Variables ¶
This section is empty.
Functions ¶
func DefaultDurationForPeriod ¶
DefaultDurationForPeriod returns the canonical duration (days) for a billing period. Unknown periods fall back to a 30-day month.
Types ¶
type Admin ¶
type Admin struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;size:64;not null" json:"username"`
PasswordHash string `gorm:"size:128;not null" json:"-"`
Role string `gorm:"size:16;default:'admin'" json:"role"` // super_admin | admin
TelegramID int64 `gorm:"index;default:0" json:"telegram_id"` // linked Telegram chat ID (0 = none)
TelegramBindToken string `gorm:"size:64;default:''" json:"-"`
TelegramBindExpiresAt *time.Time `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Admin is a manager administrator account.
type Announcement ¶
type Announcement struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
Title string `gorm:"size:255" json:"title"`
Content string `gorm:"type:text" json:"content"`
Pinned bool `gorm:"default:false" json:"pinned"`
Active bool `gorm:"default:true;index" json:"active"`
AuthorAdminID uint `json:"author_admin_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Announcement is a site-wide message authored by an admin and shown to users (e.g. in the user SPA). Pinned announcements sort first; inactive ones are hidden from users but remain editable by admins.
type BalanceTransaction ¶
type BalanceTransaction struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
UserID string `gorm:"index;size:36;not null" json:"user_id"`
Type string `gorm:"size:16;not null" json:"type"` // "credit" | "debit"
AmountCents int64 `gorm:"not null" json:"amount_cents"` // always positive
Reason string `gorm:"size:32;not null" json:"reason"`
RefOrderID string `gorm:"size:36;index" json:"ref_order_id,omitempty"`
BalanceAfter int64 `gorm:"not null" json:"balance_after"`
Remark string `json:"remark,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
BalanceTransaction is a single append-only ledger row for a user's account balance. Credit and debit rows both carry a positive AmountCents; Type indicates the direction. BalanceAfter is the running balance (in cents) immediately after this row, kept for fast, tamper-evident history without recomputing a sum.
type EmailVerification ¶
type EmailVerification struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
UserID string `gorm:"size:36;index" json:"user_id"`
Email string `gorm:"size:255" json:"email"`
Token string `gorm:"uniqueIndex;size:64" json:"-"`
Purpose string `gorm:"size:32" json:"purpose"` // "register" | "reset_password"
ExpiresAt time.Time `gorm:"index" json:"expires_at"`
ConsumedAt *time.Time `json:"consumed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
EmailVerification stores a single-use token emailed to a user to prove ownership of an address (currently used for registration activation). A token is valid while ConsumedAt is nil and ExpiresAt is in the future.
func (*EmailVerification) Consumed ¶
func (v *EmailVerification) Consumed() bool
Consumed reports whether the token has already been used.
type InviteCode ¶
type InviteCode struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
Code string `gorm:"uniqueIndex;size:64" json:"code"`
CreatedByUserID *string `gorm:"size:36;index" json:"created_by_user_id,omitempty"` // nil ⇒ admin-generated
CreatedByAdminID *uint `gorm:"index" json:"created_by_admin_id,omitempty"`
Note string `gorm:"size:255" json:"note,omitempty"`
MaxUses int `gorm:"default:1" json:"max_uses"`
UsedCount int `gorm:"default:0" json:"used_count"`
ExpiresAt *time.Time `gorm:"index" json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
InviteCode grants the holder the right to register (when registration is gated by invite) up to MaxUses times. Codes are created either by an admin (CreatedByUserID is nil) or by a regular user within their invite quota (CreatedByUserID set, and Σ UsedCount across that user's codes must stay within the quota). Consuming a code increments UsedCount; registration is rejected once UsedCount >= MaxUses or the code is expired.
func (*InviteCode) Exhausted ¶
func (c *InviteCode) Exhausted(now time.Time) bool
Exhausted reports whether the code has no remaining uses or is expired.
func (*InviteCode) Remaining ¶
func (c *InviteCode) Remaining() int
Remaining returns how many more registrations this code can sponsor.
type Node ¶
type Node struct {
ID string `gorm:"primaryKey;size:26" json:"id"` // ULID
Name string `gorm:"size:128;not null;uniqueIndex:uk_node_name" json:"name"` // display name → share-link tag; unique per node
ParentID *string `gorm:"size:26;index" json:"parent_id,omitempty"` // nil = real node; non-nil = virtual child pointing to parent Node.ID (inherits its transport config)
ParentName string `gorm:"-" json:"parent_name,omitempty"` // server-populated display name of ParentID; not persisted
Address string `gorm:"size:255;not null" json:"address"` // host:port for share links
Port int `gorm:"not null" json:"port"` // server listen port
Token string `gorm:"size:64;uniqueIndex;not null" json:"token"` // crypto-random secret
Network string `gorm:"size:16;default:'tcp'" json:"network"` // tcp|ws|xhttp
Security string `gorm:"size:16;not null" json:"security"` // none|tls|reality
Settings datatypes.JSON `gorm:"type:json" json:"settings"` // transport settings (path, x_padding_bytes, ...)
TLSConfig *datatypes.JSON `gorm:"type:json" json:"tls_settings,omitempty"`
RealityConfig *datatypes.JSON `gorm:"type:json" json:"reality_settings,omitempty"`
VLESS *datatypes.JSON `gorm:"type:json" json:"vless,omitempty"` // v2 decryption config
Flow *string `gorm:"size:32;default:''" json:"flow,omitempty"` // "" | xtls-rprx-vision
Level int `gorm:"default:0" json:"level"` // access tier; a user may use the node only if user.level >= node.level (unless explicitly overridden)
AllowInsecure bool `gorm:"default:false" json:"allow_insecure"` // toggles allowInsecure=1 in TLS links
// TrafficMultiplier scales the bytes reported by this node's users when the
// manager aggregates them (only applied on the manager side). 1 = no change;
// >1 inflates reported traffic (e.g. for billing), <1 deflates it. Virtual
// child nodes inherit their parent's multiplier.
TrafficMultiplier float64 `gorm:"default:1" json:"traffic_multiplier"`
// SpeedLimitUpBps / SpeedLimitDownBps cap this node's aggregate upload /
// download throughput in bytes/sec (0 = unlimited). Enforced by the node
// itself; virtual child nodes inherit their parent's limit.
SpeedLimitUpBps int64 `gorm:"default:0" json:"speed_limit_up_bps"`
SpeedLimitDownBps int64 `gorm:"default:0" json:"speed_limit_down_bps"`
LastSeenAt *time.Time `gorm:"index" json:"last_seen_at,omitempty"` // liveness (updated each poll)
Online bool `gorm:"-" json:"online"` // server-computed: LastSeenAt within NodeOnlineWindow
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Node is a vgate server instance the manager controls. The manager issues a public ID (ULID) and a secret Token; the node presents both on each poll. Transport/security config is stored as JSON columns and materialized into a wire.Config when the node fetches it.
type Order ¶
type Order struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
UserID string `gorm:"index;size:36;not null" json:"user_id"`
Kind string `gorm:"size:16;not null;default:'plan'" json:"kind"`
PlanID string `gorm:"index;size:36" json:"plan_id,omitempty"`
PlanPriceID string `gorm:"index;size:36" json:"plan_price_id,omitempty"`
Period string `gorm:"size:16" json:"period,omitempty"`
DurationDays int `gorm:"default:0" json:"duration_days"`
TrafficPackageID string `gorm:"index;size:36" json:"traffic_package_id,omitempty"`
Amount int64 `gorm:"not null" json:"amount"` // cents, copied from source; may be reduced by wallet deduction
PlanPriceCents int64 `gorm:"not null;default:0" json:"plan_price_cents,omitempty"` // gross plan-price cents before any wallet deduction; used for proration credit
Status string `gorm:"index;size:16;not null;default:'pending'" json:"status"`
Platform string `gorm:"index;size:16" json:"platform"` // payment gateway: alipay | manual | balance | (future)
// ExtendFromOldExpiry controls how applyPlanEffect computes the new
// ExpireAt. When true (normal purchase / renewal) the new period stacks on
// top of the existing expiry. When false (a plan upgrade that bought out
// the old period via a balance credit) the new period starts now.
ExtendFromOldExpiry bool `gorm:"not null;default:true" json:"extend_from_old_expiry"`
OutTradeNo string `gorm:"uniqueIndex;size:64;not null" json:"out_trade_no"`
TradeNo string `gorm:"size:64" json:"trade_no,omitempty"` // gateway-assigned transaction id
PaidAt *time.Time `json:"paid_at,omitempty"`
ExpiredAt *time.Time `gorm:"index" json:"expired_at,omitempty"` // cron close threshold
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Order records a single alipay purchase attempt. It is kind-aware:
- kind=plan: references PlanID + PlanPriceID; carries the chosen Period/DurationDays (copied from the price at creation).
- kind=traffic: references TrafficPackageID.
Amount is copied from the authoritative source (plan price or traffic package) at creation time; clients cannot override it.
type Plan ¶
type Plan struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
// DisplayName is an optional product name pushed to the payment gateway
// instead of the built-in default subject. Empty ⇒ the global
// payment.product_name_template (then the built-in default) is used.
DisplayName string `gorm:"size:128" json:"display_name"`
Description string `gorm:"type:text" json:"description"`
Level int `gorm:"default:0" json:"level"`
QuotaBytes int64 `gorm:"not null;default:0" json:"quota_bytes"`
SpeedLimitUpBps int64 `gorm:"not null;default:0" json:"speed_limit_up_bps"`
SpeedLimitDownBps int64 `gorm:"not null;default:0" json:"speed_limit_down_bps"`
Enabled bool `gorm:"not null" json:"enabled"`
// AllowRenewOffShelf lets a user who already owns this plan renew it even
// after the plan is disabled (taken off the shelf). New users can never
// purchase an off-shelf plan regardless of this flag. Admins are exempt.
AllowRenewOffShelf bool `gorm:"not null;default:false" json:"allow_renew_off_shelf"`
// Prices stores the plan's billing-period price options as a JSON array.
// It replaces the former plan_prices table (which is kept read-only for
// historical Order.PlanPriceID references).
Prices PlanPrices `gorm:"type:json" json:"prices,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Plan is a purchasable product group: a bundle of traffic quota + user level that a user buys via an alipay order. Pricing is NOT stored here — it lives in the related PlanPrice rows so a single plan can be offered at different price points for different billing periods (month/quarter/half-year/year).
type PlanPrice ¶
type PlanPrice struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
PlanID string `gorm:"index;size:36;not null" json:"plan_id"`
Period string `gorm:"size:16;not null" json:"period"` // month|quarter|half_year|year
Price int64 `gorm:"not null" json:"price"` // cents (server truth)
DurationDays int `gorm:"not null" json:"duration_days"` // 30|90|180|365
Sort int `gorm:"default:0" json:"sort"`
Enabled bool `gorm:"not null" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
PlanPrice is the legacy table-backed billing-period price point. It is retained only so historical plan_prices rows (referenced by old Orders) remain readable. New plans store pricing in Plan.Prices (of type PlanPrices / PlanPriceEntry) as a JSON column.
type PlanPriceEntry ¶
type PlanPriceEntry struct {
Period string `json:"period"` // month|quarter|half_year|year
Price int64 `json:"price"` // cents
DurationDays int `json:"duration_days"` // 30|90|180|365
Sort int `json:"sort"`
Enabled bool `json:"enabled"`
}
PlanPriceEntry is a billing-period price point that lives as a JSON array element inside Plan.Prices (replacing the separate plan_prices table). It has no database identity of its own — prices are identified by Period within a plan.
type PlanPrices ¶
type PlanPrices []PlanPriceEntry
PlanPrices is a JSON column containing a plan's pricing options. It implements driver.Valuer / sql.Scanner so GORM can serialize it to a single JSON column on the plans table.
func (*PlanPrices) Scan ¶
func (p *PlanPrices) Scan(value any) error
type RedemptionCode ¶
type RedemptionCode struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
Code string `gorm:"uniqueIndex;size:64" json:"code"`
Type string `gorm:"size:16;index" json:"type"` // traffic|duration|plan
MaxUses int `gorm:"default:1" json:"max_uses"`
UsedCount int `gorm:"default:0" json:"used_count"`
ExpiresAt *time.Time `gorm:"index" json:"expires_at,omitempty"`
Note string `gorm:"size:255" json:"note,omitempty"`
// Benefit parameters, interpreted according to Type.
QuotaBytes int64 `gorm:"default:0" json:"quota_bytes,omitempty"` // type=traffic
DurationDays int `gorm:"default:0" json:"duration_days,omitempty"` // type=duration
PlanID string `gorm:"size:36;index" json:"plan_id,omitempty"` // type=plan
CreatedByAdminID *uint `gorm:"index" json:"created_by_admin_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
RedemptionCode is an admin-issued token that grants a benefit when redeemed by a user. Type selects the benefit; the relevant *Params fields carry the benefit's parameters. A code may be redeemed up to MaxUses times, once per distinct user (enforced via RedemptionRecord), and never after ExpiresAt.
func (*RedemptionCode) Exhausted ¶
func (c *RedemptionCode) Exhausted(now time.Time) bool
Exhausted reports whether the code has no remaining uses or is expired.
func (*RedemptionCode) Remaining ¶
func (c *RedemptionCode) Remaining() int
Remaining returns how many more distinct users can redeem this code.
type RedemptionRecord ¶
type RedemptionRecord struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
CodeID string `gorm:"size:36;uniqueIndex:uk_code_user;index" json:"code_id"`
UserID string `gorm:"size:36;uniqueIndex:uk_code_user;index" json:"user_id"`
Type string `gorm:"size:16" json:"type"` // snapshot of code.Type
RedeemedAt time.Time `json:"redeemed_at"`
}
RedemptionRecord is one user redemption of a code. The (code_id, user_id) unique pair prevents a single user from draining a multi-use code, so MaxUses counts distinct users rather than total redemptions.
type RefreshToken ¶
type RefreshToken struct {
ID string `gorm:"primaryKey;size:32"` // crypto-random opaque token
AdminID uint `gorm:"index;not null"`
ExpiresAt time.Time `gorm:"index"`
Revoked bool `gorm:"default:false"`
CreatedAt time.Time
}
RefreshToken is a revocable admin refresh token (DB-stored, not a pure JWT).
type SystemConfig ¶
type SystemConfig struct {
Key string `gorm:"primaryKey;size:64"`
Value string `gorm:"type:text"`
UpdatedAt time.Time
}
SystemConfig stores runtime key/value settings (default_sync_interval, jwt_ttl, etc.). The JWT secret stays in config.yml, NOT here.
type Ticket ¶
type Ticket struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
UserID string `gorm:"index;size:36;not null" json:"user_id"`
Subject string `gorm:"size:255;not null" json:"subject"`
Priority string `gorm:"size:16;default:'normal';index" json:"priority"`
Status string `gorm:"size:16;default:'open';index" json:"status"`
// NotifyMethod is the ticket owner's preferred channel for being notified of
// admin replies / status changes on THIS ticket. Empty/"none" = no notification.
// "email" / "telegram" select the channel. Defaults to "telegram" when the
// owner has a linked Telegram chat, else "none".
NotifyMethod string `gorm:"size:16;default:''" json:"notify_method"`
UserEmail string `gorm:"-" json:"user_email,omitempty"` // admin display only, not persisted
// LastSender is the role of the author of the most recent message on the
// ticket (model.TicketSenderUser | model.TicketSenderAdmin). It is
// denormalized so unread detection can tell, without a sub-query, which
// side spoke last. Not exposed in JSON.
LastSender string `gorm:"size:8;default:''" json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Ticket is a support work-order opened by a user and handled by admins.
type TicketMessage ¶
type TicketMessage struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
TicketID string `gorm:"index;size:36;not null" json:"ticket_id"`
Sender string `gorm:"size:8;not null" json:"sender"` // user | admin
SenderID string `gorm:"size:36" json:"sender_id"` // user id or admin id
Content string `gorm:"type:text;not null" json:"content"`
CreatedAt time.Time `json:"created_at"`
}
TicketMessage is a single message in a ticket conversation thread.
type TicketReadState ¶
type TicketReadState struct {
TicketID string `gorm:"primaryKey;size:36" json:"ticket_id"`
Recipient string `gorm:"primaryKey;size:64" json:"recipient"`
LastReadAt time.Time `json:"last_read_at"`
}
TicketReadState records, per recipient, when they last opened a ticket. It drives the unread-dot in the frontends: a ticket counts as unread for a recipient while its last activity is newer than their LastReadAt and the last speaker was the other side. Recipient is "u:<userID>" for users, or "admin" for a single global state shared by all admins.
func (TicketReadState) TableName ¶
func (TicketReadState) TableName() string
TableName pins the read-state table name (GORM would otherwise pluralize).
type TrafficGrant ¶
type TrafficGrant struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
UserID string `gorm:"index;not null" json:"user_id"`
Source string `gorm:"size:16;not null" json:"source"` // "traffic_package" | "redemption"
SourceID string `gorm:"size:36" json:"source_id"` // traffic_package id or redemption_code id
// Name is a denormalized display label (the traffic-package Name, or the
// redemption Code) so clients can render the grant without extra lookups.
Name string `gorm:"size:128" json:"name"`
QuotaBytes int64 `gorm:"not null" json:"quota_bytes"`
// UsedBytes is the traffic already consumed from this grant. Traffic is
// charged to grants FIFO across a user's active grants, so UsedBytes lets
// clients show the remaining (QuotaBytes - UsedBytes) precisely.
UsedBytes int64 `gorm:"default:0" json:"used_bytes"`
GrantedAt time.Time `gorm:"not null" json:"granted_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
TrafficGrant records a single one-time traffic-quota grant applied to a user, whether from a purchased TrafficPackage or a redeemed traffic-type RedemptionCode. Grants are permanent add-ons: once granted, the bonus lives on the user's traffic_quota_bytes until the user is deleted, and it is never partially reclaimed. Traffic is charged to grants FIFO across a user's active grants so each grant's remaining (QuotaBytes - UsedBytes) can be shown precisely.
func (*TrafficGrant) RemainingBytes ¶
func (g *TrafficGrant) RemainingBytes() int64
RemainingBytes returns the un-consumed quota of the grant (never negative).
type TrafficHourlyStat ¶
type TrafficHourlyStat struct {
UserID string `gorm:"primaryKey;size:36;index"`
Hour time.Time `gorm:"primaryKey;index"` // hour bucket (UTC, truncated to hour)
UpTotal int64 `gorm:"default:0"` // cumulative up_total at this hour
DownTotal int64 `gorm:"default:0"` // cumulative down_total at this hour
CreatedAt time.Time
}
TrafficHourlyStat stores a per-user cumulative-traffic snapshot at each hour boundary. The hourly aggregation job upserts one row per (user, hour). 24h usage is computed by subtracting the snapshot from 24 hours ago from the current cumulative total. Rows older than 48 hours are pruned.
type TrafficPackage ¶
type TrafficPackage struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
// DisplayName is an optional product name pushed to the payment gateway
// instead of the built-in default (the package Name). Empty ⇒ the global
// payment.product_name_template (then the built-in default) is used.
DisplayName string `gorm:"size:128" json:"display_name"`
Price int64 `gorm:"not null" json:"price"` // cents (server truth)
QuotaBytes int64 `gorm:"not null" json:"quota_bytes"`
Description string `gorm:"type:text" json:"description"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
TrafficPackage is a one-time, non-recurring traffic add-on a user buys via an alipay order. Unlike a Plan it does not carry a user level and has no fixed billing period. The granted quota is permanent (a TrafficGrant with no ExpireAt) — it is never auto-reclaimed and survives base-plan resets.
type User ¶
type User struct {
ID string `gorm:"primaryKey;size:36" json:"id"` // stable internal PK (NOT the VLESS credential)
// Credential is the rotatable VLESS UUID sent to nodes (wire.User.ID) and
// embedded in the subscription link. It is decoupled from ID so a leaked
// credential can be regenerated without touching the primary key.
Credential string `gorm:"uniqueIndex;size:36" json:"credential"`
// CurrentProductID is the id of the user's currently active plan, set when
// a paid plan order's effect is applied. Traffic packages are add-ons and
// never become the current product, so this is always a plan id or empty;
// nullable and not cleared on expiry.
CurrentProductID string `gorm:"size:36;index" json:"current_product_id,omitempty"`
// CurrentProductName is the display name of CurrentProductID, populated by
// the service layer (not stored). Empty when no active product or the
// product no longer exists.
CurrentProductName string `gorm:"-" json:"current_product_name,omitempty"`
Email string `gorm:"uniqueIndex;size:255;not null" json:"email"`
Username *string `gorm:"uniqueIndex;size:64" json:"username,omitempty"`
PasswordHash *string `gorm:"size:128" json:"-"` // bcrypt, nullable
// HasPassword is a derived flag (not stored) exposing whether the user has
// a password set, so the client can decide whether to prompt for the
// current password when changing it.
HasPassword bool `gorm:"-" json:"has_password,omitempty"`
SubToken string `gorm:"uniqueIndex;size:32;not null" json:"sub_token"` // crypto-random share-URL credential
Level int `gorm:"default:0" json:"level"`
ExpireAt *time.Time `gorm:"index" json:"expire_at,omitempty"`
QuotaBytes int64 `gorm:"default:0" json:"quota_bytes"` // base traffic cap in bytes: -1 = unlimited, 0 = no quota (blocked), >0 = capped. Set by plans; traffic-package/redemption bonuses live in TrafficQuotaBytes.
TrafficQuotaBytes int64 `gorm:"default:0" json:"traffic_quota_bytes"` // sum of active (non-expired) traffic-package / redemption bonuses, reclaimed on expiry
// PackageUsedBytes is the cumulative traffic (bytes) charged to
// traffic-package / redemption grants (FIFO). It persists across base-quota
// resets (monthly, manual, or plan renewal) so a reset renews the base
// window without refunding package traffic already consumed. The remaining
// package pool is TrafficQuotaBytes; per-grant remaining is on TrafficGrant.
PackageUsedBytes int64 `gorm:"default:0" json:"package_used_bytes"`
// TrafficGrants lists the user's active (non-reclaimed) grants, populated
// by UserService.Get for the profile / admin detail responses. Not stored.
TrafficGrants []TrafficGrant `gorm:"-" json:"traffic_grants,omitempty"`
QuotaResetEnabled bool `gorm:"default:false" json:"quota_reset_enabled"` // participates in global monthly reset (reset day from system_config)
// BalanceCents is the user's spendable account-balance wallet (cents). It
// can pay for any purchase (plans, traffic packages) and is credited
// when a plan change refunds the remaining value of the old plan.
BalanceCents int64 `gorm:"default:0" json:"balance_cents"`
// CurrentPlanPaidCents / CurrentPlanDurationDays record what the user paid
// for the CURRENT plan entitlement (gross cents + the duration of that
// purchase). They enable per-day amortization so a mid-period plan change
// can credit the old plan's remaining value. Only meaningful when the user
// has a current plan (CurrentProductID is set). Exposed so the change-plan
// dialog can preview the credit client-side.
CurrentPlanPaidCents int64 `gorm:"default:0" json:"current_plan_paid_cents"`
CurrentPlanDurationDays int `gorm:"default:0" json:"current_plan_duration_days"`
// SpeedLimitUpBps / SpeedLimitDownBps cap this user's upload / download
// throughput in bytes/sec (0 = unlimited). Enforced by the node; the
// effective rate is min(node global limit, this per-user limit).
SpeedLimitUpBps int64 `gorm:"default:0" json:"speed_limit_up_bps"`
SpeedLimitDownBps int64 `gorm:"default:0" json:"speed_limit_down_bps"`
UpTotal int64 `gorm:"default:0" json:"up_total"`
DownTotal int64 `gorm:"default:0" json:"down_total"`
LastTrafficAt *time.Time `gorm:"index" json:"last_traffic_at,omitempty"` // last node-reported traffic delta
Enabled bool `gorm:"default:true" json:"enabled"`
// EmailVerified is set true once the user proves ownership of Email (e.g.
// via the registration verification link). Surfaced to admins so pending
// (registered-but-unverified) accounts are visible.
EmailVerified bool `gorm:"default:false" json:"email_verified"`
// MaxInvites caps how many successful registrations this user may sponsor
// via invite codes they generate. 0 means "use the global default"
// (system_config invite.default_user_quota). Admin-set overrides apply.
MaxInvites int `gorm:"default:0" json:"max_invites"`
// Telegram integration fields. TelegramID is the chat id of the user's
// linked Telegram account (0 = not linked). TelegramNotify gates
// announcement broadcasts; it defaults to true once linked so the user
// receives announcements unless they opt out. The bind token is a
// one-time code (with expiry) exchanged via /start <code> to link the
// account; both are cleared after a successful bind.
TelegramID int64 `gorm:"index" json:"telegram_id"`
TelegramBoundAt *time.Time `json:"telegram_bound_at,omitempty"`
TelegramNotify bool `gorm:"default:true" json:"telegram_notify"`
TelegramBindToken string `gorm:"size:32;index" json:"-"`
TelegramBindExpiresAt *time.Time `json:"-"`
// ReminderChannel selects how this user receives traffic reminders.
// "" means auto (Telegram if linked, else email if verified, else none);
// explicit values are "email", "telegram", or "none" (disabled). The
// thresholds and cooldown are configured globally by an admin.
ReminderChannel string `gorm:"size:8;default:''" json:"reminder_channel"`
// LastTrafficReminderAt is the timestamp of the last traffic reminder sent
// to this user; it enforces the global cooldown (reminder.cooldown_days)
// so a user is not reminded more than once per cooldown window.
LastTrafficReminderAt *time.Time `json:"last_traffic_reminder_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
User is a VLESS end-user. ID is the VLESS UUID credential; Email is the traffic-accounting key. UpTotal/DownTotal are cumulative (aggregated from node-reported deltas). SubToken authenticates the share URL; PasswordHash is optional (enables /user/login when set).
func (User) BaseUsedBytes ¶
BaseUsedBytes returns the traffic charged against the base plan quota only (total used minus the package-used portion). Floored at 0. The package-used portion survives base resets, so this isolates the base window's consumption.
func (User) EffectiveQuotaBytes ¶
EffectiveQuotaBytes returns the user's total traffic cap, combining the base plan quota (QuotaBytes, with -1 = unlimited) and any active traffic-package / redemption bonuses (TrafficQuotaBytes). A base of -1 stays unlimited regardless of bonuses; otherwise the two are summed.
type UserNode ¶
type UserNode struct {
UserID string `gorm:"primaryKey;size:36"`
NodeID string `gorm:"primaryKey;size:26"`
User User `gorm:"foreignKey:UserID"`
Node Node `gorm:"foreignKey:NodeID"`
// Override lets an admin grant a specific user access to a node whose level
// exceeds the user's level (the default gate is node.level <= user.level).
Override bool `gorm:"default:false" json:"override"`
CreatedAt time.Time
}
UserNode is the admin override / exception table: it grants a specific user access to a node whose level is above the user's level (the default gate is node.level <= user.level, enforced without any row here). Within-level nodes are usable by virtue of the level tier and need no UserNode row. Override marks a grant that bypasses the level gate.
type UserNodeTraffic ¶
type UserNodeTraffic struct {
UserID string `gorm:"primaryKey;size:36"`
NodeID string `gorm:"primaryKey;size:26"`
UpTotal int64 `gorm:"default:0"`
DownTotal int64 `gorm:"default:0"`
CreatedAt time.Time
UpdatedAt time.Time
}
UserNodeTraffic tracks cumulative per-node-per-user traffic, enabling the admin traffic view to filter by node_id. (Per-user totals also live on User.) Populated atomically from node-reported deltas.
func (UserNodeTraffic) TableName ¶
func (UserNodeTraffic) TableName() string
TableName fixes the table name (GORM would otherwise pluralize to the grammatically-wrong "user_node_traffics").