model

package
v0.0.0-...-2c9f6af Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Index

Constants

View Source
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.

View Source
const (
	BalanceTypeCredit = "credit"
	BalanceTypeDebit  = "debit"
)

Balance transaction types.

View Source
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"
)
View Source
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.

View Source
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.

View Source
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"
)
View Source
const (
	GrantSourceTrafficPackage = "traffic_package"
	GrantSourceRedemption     = "redemption"
)

Traffic grant sources.

View Source
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

func DefaultDurationForPeriod(period string) int

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.

func (*EmailVerification) Expired

func (v *EmailVerification) Expired(now time.Time) bool

Expired reports whether the token is past its expiry.

func (*EmailVerification) Valid

func (v *EmailVerification) Valid(now time.Time) bool

Valid reports whether the token can still be redeemed.

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.

func (*Node) IsOnline

func (n *Node) IsOnline() bool

IsOnline reports whether the node polled within NodeOnlineWindow.

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

func (PlanPrices) Value

func (p PlanPrices) Value() (driver.Value, 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

func (u User) BaseUsedBytes() int64

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

func (u User) EffectiveQuotaBytes() int64

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").

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL