Files
3x-ui-agent/internal/web/service/probe/meta.go
T

162 lines
5.1 KiB
Go

package probe
import (
"database/sql"
"strconv"
"strings"
)
// Meta is one agent's probe display/operations metadata (hub node row's
// operator-owned half, ITEM7-SCOPE §5.2): what the public page shows
// beyond the machine's own reports — price/expiry/plan figures, the
// public switch and ordering, grouping and the public remark, the
// notify opt-in and the hand-set address/country pins. Rows are
// ensured with hub defaults on the agent's first report; the editing
// surface (B3) reads and writes them here.
type Meta struct {
AgentID int64
Price float64
Currency string
BillingCycle string
ExpiresAt *string // "2006-01-02" or nil
TrafficLimit int64
TrafficMode string
TrafficResetDay int64
Public bool
Sort int64
GroupName string
PublicRemark string
Notify bool
CountryPin string
IPv4Pin string
IPv6Pin string
}
// DefaultMeta returns the metadata of an agent that has no row yet —
// the same values the row would carry once ensured (the probe_meta
// DDL defaults).
func DefaultMeta(agentID int64) Meta {
return Meta{
AgentID: agentID,
Currency: "USD",
BillingCycle: "monthly",
TrafficMode: "sum",
TrafficResetDay: 1,
Public: true,
}
}
const metaColumns = `agent_id, price, currency, billing_cycle, expires_at,
traffic_limit, traffic_mode, traffic_reset_day, public, sort,
group_name, public_remark, notify, country_pin, ipv4_pin, ipv6_pin`
func scanMeta(sc interface{ Scan(...any) error }) (Meta, error) {
var m Meta
var expires sql.NullString
var public, notify int64
if err := sc.Scan(&m.AgentID, &m.Price, &m.Currency, &m.BillingCycle, &expires,
&m.TrafficLimit, &m.TrafficMode, &m.TrafficResetDay, &public, &m.Sort,
&m.GroupName, &m.PublicRemark, &notify, &m.CountryPin, &m.IPv4Pin, &m.IPv6Pin); err != nil {
return Meta{}, err
}
if expires.Valid && strings.TrimSpace(expires.String) != "" {
v := expires.String
m.ExpiresAt = &v
}
m.Public = public != 0
m.Notify = notify != 0
return m, nil
}
// Meta reads one agent's metadata row. ok is false when the agent has
// no row yet (it never reported); callers then use DefaultMeta.
func (s *Store) Meta(agentID int64) (Meta, bool, error) {
row := s.db.QueryRow(`SELECT `+metaColumns+` FROM probe_meta WHERE agent_id = ?`, agentID)
m, err := scanMeta(row)
if err == sql.ErrNoRows {
return Meta{}, false, nil
}
if err != nil {
return Meta{}, false, err
}
return m, true, nil
}
// AllMeta reads every metadata row, keyed by agent id.
func (s *Store) AllMeta() (map[int64]Meta, error) {
rows, err := s.db.Query(`SELECT ` + metaColumns + ` FROM probe_meta`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[int64]Meta)
for rows.Next() {
m, err := scanMeta(rows)
if err != nil {
return nil, err
}
out[m.AgentID] = m
}
return out, rows.Err()
}
// UpsertMeta writes one agent's metadata row (insert or full replace).
func (s *Store) UpsertMeta(m Meta) error {
var expires any
if m.ExpiresAt != nil && strings.TrimSpace(*m.ExpiresAt) != "" {
expires = *m.ExpiresAt
}
_, err := s.db.Exec(
`INSERT INTO probe_meta (`+metaColumns+`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(agent_id) DO UPDATE SET
price=excluded.price, currency=excluded.currency,
billing_cycle=excluded.billing_cycle, expires_at=excluded.expires_at,
traffic_limit=excluded.traffic_limit, traffic_mode=excluded.traffic_mode,
traffic_reset_day=excluded.traffic_reset_day, public=excluded.public,
sort=excluded.sort, group_name=excluded.group_name,
public_remark=excluded.public_remark, notify=excluded.notify,
country_pin=excluded.country_pin, ipv4_pin=excluded.ipv4_pin,
ipv6_pin=excluded.ipv6_pin`,
m.AgentID, m.Price, m.Currency, m.BillingCycle, expires,
m.TrafficLimit, m.TrafficMode, m.TrafficResetDay, boolToInt(m.Public), m.Sort,
m.GroupName, m.PublicRemark, boolToInt(m.Notify), m.CountryPin, m.IPv4Pin, m.IPv6Pin)
return err
}
func boolToInt(b bool) int64 {
if b {
return 1
}
return 0
}
// DeleteAgentData removes every probe row of an agent that no longer
// exists: metadata, the traffic ledger and both history tiers. The
// hub keeps history inside its own node table; here probe.db is a
// separate file, so the panel-side agent deletion calls this to keep
// the two stores in step (B2 report §6 cleanup point).
func (s *Store) DeleteAgentData(agentID int64) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
for _, table := range []string{
"probe_meta", "probe_traffic", "probe_metric",
"probe_metric_hour", "probe_ping_record", "probe_ping_hour",
"probe_ping_node",
} {
if _, err := tx.Exec(`DELETE FROM `+table+` WHERE agent_id = ?`, agentID); err != nil {
return err
}
}
// The B6 offline mark is keyed per agent in probe_setting; the
// traffic steps are one shared JSON value the sweep prunes to
// live agents on its next write, so only the mark is struck here.
if _, err := tx.Exec(`DELETE FROM probe_setting WHERE key = ?`,
"notify_down:"+strconv.FormatInt(agentID, 10)); err != nil {
return err
}
return tx.Commit()
}