model.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. // Package model defines the database models and data structures used by the 3x-ui panel.
  2. package model
  3. import (
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "strings"
  8. "github.com/mhsanaei/3x-ui/v3/util/json_util"
  9. "github.com/mhsanaei/3x-ui/v3/xray"
  10. )
  11. // Protocol represents the protocol type for Xray inbounds.
  12. type Protocol string
  13. // Protocol constants for different Xray inbound protocols.
  14. // Hysteria v2 is not a distinct protocol — it is plain "hysteria"
  15. // with streamSettings.version = 2. The share-link URI scheme
  16. // "hysteria2://" is independent of this and is still emitted by the
  17. // link generator when the stream version is 2.
  18. const (
  19. VMESS Protocol = "vmess"
  20. VLESS Protocol = "vless"
  21. Tunnel Protocol = "tunnel"
  22. HTTP Protocol = "http"
  23. Trojan Protocol = "trojan"
  24. Shadowsocks Protocol = "shadowsocks"
  25. Mixed Protocol = "mixed"
  26. WireGuard Protocol = "wireguard"
  27. Hysteria Protocol = "hysteria"
  28. )
  29. // User represents a user account in the 3x-ui panel.
  30. type User struct {
  31. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  32. Username string `json:"username"`
  33. Password string `json:"password"`
  34. LoginEpoch int64 `json:"-" gorm:"default:0"`
  35. }
  36. // Inbound represents an Xray inbound configuration with traffic statistics and settings.
  37. type Inbound struct {
  38. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
  39. UserId int `json:"-"` // Associated user ID
  40. Up int64 `json:"up" form:"up"` // Upload traffic in bytes
  41. Down int64 `json:"down" form:"down"` // Download traffic in bytes
  42. Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
  43. Remark string `json:"remark" form:"remark"` // Human-readable remark
  44. Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
  45. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  46. TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule
  47. LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
  48. ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
  49. // Xray configuration fields
  50. Listen string `json:"listen" form:"listen"`
  51. Port int `json:"port" form:"port" validate:"gte=1,lte=65535"`
  52. Protocol Protocol `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan shadowsocks wireguard hysteria http mixed tunnel"`
  53. Settings string `json:"settings" form:"settings"`
  54. StreamSettings string `json:"streamSettings" form:"streamSettings"`
  55. Tag string `json:"tag" form:"tag" gorm:"unique"`
  56. Sniffing string `json:"sniffing" form:"sniffing"`
  57. NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"`
  58. // FallbackParent is populated by the API layer when this inbound is
  59. // attached as a fallback child of a VLESS/Trojan TCP-TLS master.
  60. // The frontend uses it to rewrite client-share links so they advertise
  61. // the master's externally reachable endpoint instead of the child's
  62. // loopback listen. Not persisted.
  63. FallbackParent *FallbackParentInfo `json:"fallbackParent,omitempty" gorm:"-"`
  64. }
  65. // FallbackParentInfo carries everything the frontend needs to rewrite a
  66. // child inbound's client link: where to connect (the master's address
  67. // and port) and which path matched on the master's fallbacks array.
  68. // The frontend already has the master inbound in its dbInbounds list,
  69. // so we only ship identifiers + the match path here.
  70. type FallbackParentInfo struct {
  71. MasterId int `json:"masterId"`
  72. Path string `json:"path,omitempty"`
  73. }
  74. // OutboundTraffics tracks traffic statistics for Xray outbound connections.
  75. type OutboundTraffics struct {
  76. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  77. Tag string `json:"tag" form:"tag" gorm:"unique"`
  78. Up int64 `json:"up" form:"up" gorm:"default:0"`
  79. Down int64 `json:"down" form:"down" gorm:"default:0"`
  80. Total int64 `json:"total" form:"total" gorm:"default:0"`
  81. }
  82. // InboundClientIps stores IP addresses associated with inbound clients for access control.
  83. type InboundClientIps struct {
  84. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  85. ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
  86. Ips string `json:"ips" form:"ips"`
  87. }
  88. // MarshalJSON emits the Ips column as a real JSON array instead of an escaped
  89. // JSON-text string. Empty or unparseable storage renders as null so API
  90. // consumers don't have to special-case the legacy double-encoded shape.
  91. func (ic InboundClientIps) MarshalJSON() ([]byte, error) {
  92. type alias InboundClientIps
  93. return json.Marshal(struct {
  94. alias
  95. Ips json.RawMessage `json:"ips"`
  96. }{
  97. alias: alias(ic),
  98. Ips: jsonStringFieldToRaw(ic.Ips),
  99. })
  100. }
  101. // UnmarshalJSON accepts ips as either a JSON array (modern shape) or a
  102. // JSON-encoded string (legacy shape), normalising back to the JSON-text the
  103. // column stores.
  104. func (ic *InboundClientIps) UnmarshalJSON(data []byte) error {
  105. type alias InboundClientIps
  106. aux := struct {
  107. *alias
  108. Ips json.RawMessage `json:"ips"`
  109. }{
  110. alias: (*alias)(ic),
  111. }
  112. if err := json.Unmarshal(data, &aux); err != nil {
  113. return err
  114. }
  115. ic.Ips = jsonStringFieldFromRaw(aux.Ips)
  116. return nil
  117. }
  118. // HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
  119. type HistoryOfSeeders struct {
  120. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  121. SeederName string `json:"seederName"`
  122. }
  123. type ApiToken struct {
  124. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  125. Name string `json:"name" gorm:"uniqueIndex;not null"`
  126. Token string `json:"token" gorm:"not null"`
  127. Enabled bool `json:"enabled" gorm:"default:true"`
  128. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  129. }
  130. // MarshalJSON emits settings, streamSettings, and sniffing as nested JSON
  131. // objects rather than escaped strings, so API consumers don't need to JSON.parse
  132. // a string inside a string. Empty fields render as null; fields whose stored
  133. // text isn't valid JSON fall back to a JSON-encoded string so no data is lost.
  134. func (i Inbound) MarshalJSON() ([]byte, error) {
  135. type alias Inbound
  136. return json.Marshal(struct {
  137. alias
  138. Settings json.RawMessage `json:"settings"`
  139. StreamSettings json.RawMessage `json:"streamSettings"`
  140. Sniffing json.RawMessage `json:"sniffing"`
  141. }{
  142. alias: alias(i),
  143. Settings: jsonStringFieldToRaw(i.Settings),
  144. StreamSettings: jsonStringFieldToRaw(i.StreamSettings),
  145. Sniffing: jsonStringFieldToRaw(i.Sniffing),
  146. })
  147. }
  148. // UnmarshalJSON accepts settings, streamSettings, and sniffing as either a raw
  149. // JSON object/array (the modern shape MarshalJSON emits) or a JSON-encoded
  150. // string (the legacy shape). Either form is normalised back to the JSON-text
  151. // string the DB column stores.
  152. func (i *Inbound) UnmarshalJSON(data []byte) error {
  153. type alias Inbound
  154. aux := struct {
  155. *alias
  156. Settings json.RawMessage `json:"settings"`
  157. StreamSettings json.RawMessage `json:"streamSettings"`
  158. Sniffing json.RawMessage `json:"sniffing"`
  159. }{
  160. alias: (*alias)(i),
  161. }
  162. if err := json.Unmarshal(data, &aux); err != nil {
  163. return err
  164. }
  165. i.Settings = jsonStringFieldFromRaw(aux.Settings)
  166. i.StreamSettings = jsonStringFieldFromRaw(aux.StreamSettings)
  167. i.Sniffing = jsonStringFieldFromRaw(aux.Sniffing)
  168. return nil
  169. }
  170. func jsonStringFieldToRaw(s string) json.RawMessage {
  171. trimmed := strings.TrimSpace(s)
  172. if trimmed == "" {
  173. return json.RawMessage("null")
  174. }
  175. if json.Valid([]byte(trimmed)) {
  176. return json.RawMessage(trimmed)
  177. }
  178. b, _ := json.Marshal(s)
  179. return b
  180. }
  181. func jsonStringFieldFromRaw(r json.RawMessage) string {
  182. trimmed := bytes.TrimSpace(r)
  183. if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
  184. return ""
  185. }
  186. if trimmed[0] == '"' {
  187. var s string
  188. if err := json.Unmarshal(trimmed, &s); err == nil {
  189. return s
  190. }
  191. }
  192. return string(trimmed)
  193. }
  194. // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
  195. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
  196. listen := i.Listen
  197. if listen == "" {
  198. listen = "0.0.0.0"
  199. }
  200. listen = fmt.Sprintf("\"%v\"", listen)
  201. protocol := string(i.Protocol)
  202. return &xray.InboundConfig{
  203. Listen: json_util.RawMessage(listen),
  204. Port: i.Port,
  205. Protocol: protocol,
  206. Settings: json_util.RawMessage(i.Settings),
  207. StreamSettings: json_util.RawMessage(i.StreamSettings),
  208. Tag: i.Tag,
  209. Sniffing: json_util.RawMessage(i.Sniffing),
  210. }
  211. }
  212. // Setting stores key-value configuration settings for the 3x-ui panel.
  213. type Setting struct {
  214. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  215. Key string `json:"key" form:"key"`
  216. Value string `json:"value" form:"value"`
  217. }
  218. // Node represents a remote 3x-ui panel registered with the central panel.
  219. // The central panel polls each node's existing /panel/api/server/status
  220. // endpoint over HTTP using the per-node ApiToken to populate the runtime
  221. // status fields below.
  222. type Node struct {
  223. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  224. Name string `json:"name" form:"name" gorm:"uniqueIndex" validate:"required"`
  225. Remark string `json:"remark" form:"remark"`
  226. Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https"`
  227. Address string `json:"address" form:"address" validate:"required"`
  228. Port int `json:"port" form:"port" validate:"gte=1,lte=65535"`
  229. BasePath string `json:"basePath" form:"basePath"`
  230. ApiToken string `json:"apiToken" form:"apiToken" validate:"required"`
  231. Enable bool `json:"enable" form:"enable" gorm:"default:true"`
  232. AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress" gorm:"default:false"`
  233. // Heartbeat-updated fields. UpdatedAt advances on every probe even when
  234. // the row is otherwise unchanged so the UI's "last seen" tooltip is
  235. // truthful without us having to read LastHeartbeat separately.
  236. Status string `json:"status" gorm:"default:unknown"` // online|offline|unknown
  237. LastHeartbeat int64 `json:"lastHeartbeat"` // unix seconds, 0 = never
  238. LatencyMs int `json:"latencyMs"`
  239. XrayVersion string `json:"xrayVersion"`
  240. PanelVersion string `json:"panelVersion" gorm:"column:panel_version"`
  241. CpuPct float64 `json:"cpuPct"`
  242. MemPct float64 `json:"memPct"`
  243. UptimeSecs uint64 `json:"uptimeSecs"`
  244. LastError string `json:"lastError"`
  245. InboundCount int `json:"inboundCount" gorm:"-"`
  246. ClientCount int `json:"clientCount" gorm:"-"`
  247. OnlineCount int `json:"onlineCount" gorm:"-"`
  248. DepletedCount int `json:"depletedCount" gorm:"-"`
  249. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  250. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  251. }
  252. type CustomGeoResource struct {
  253. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  254. Type string `json:"type" gorm:"not null;uniqueIndex:idx_custom_geo_type_alias;column:geo_type"`
  255. Alias string `json:"alias" gorm:"not null;uniqueIndex:idx_custom_geo_type_alias"`
  256. Url string `json:"url" gorm:"not null"`
  257. LocalPath string `json:"localPath" gorm:"column:local_path"`
  258. LastUpdatedAt int64 `json:"lastUpdatedAt" gorm:"default:0;column:last_updated_at"`
  259. LastModified string `json:"lastModified" gorm:"column:last_modified"`
  260. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli;column:created_at"`
  261. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli;column:updated_at"`
  262. }
  263. type ClientReverse struct {
  264. Tag string `json:"tag"`
  265. }
  266. // Client represents a client configuration for Xray inbounds with traffic limits and settings.
  267. type Client struct {
  268. ID string `json:"id,omitempty"` // Unique client identifier
  269. Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
  270. Password string `json:"password,omitempty"` // Client password
  271. Flow string `json:"flow,omitempty"` // Flow control (XTLS)
  272. Reverse *ClientReverse `json:"reverse,omitempty"` // VLESS simple reverse proxy settings
  273. Auth string `json:"auth,omitempty"` // Auth password (Hysteria)
  274. Email string `json:"email"` // Client email identifier
  275. LimitIP int `json:"limitIp"` // IP limit for this client
  276. TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
  277. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  278. Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
  279. TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
  280. SubID string `json:"subId" form:"subId"` // Subscription identifier
  281. Comment string `json:"comment" form:"comment"` // Client comment
  282. Reset int `json:"reset" form:"reset"` // Reset period in days
  283. CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
  284. UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
  285. }
  286. type ClientRecord struct {
  287. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  288. Email string `json:"email" gorm:"uniqueIndex;not null"`
  289. SubID string `json:"subId" gorm:"index;column:sub_id"`
  290. UUID string `json:"uuid" gorm:"column:uuid"`
  291. Password string `json:"password"`
  292. Auth string `json:"auth"`
  293. Flow string `json:"flow"`
  294. Security string `json:"security"`
  295. Reverse string `json:"reverse" gorm:"column:reverse"`
  296. LimitIP int `json:"limitIp" gorm:"column:limit_ip"`
  297. TotalGB int64 `json:"totalGB" gorm:"column:total_gb"`
  298. ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
  299. Enable bool `json:"enable" gorm:"default:true"`
  300. TgID int64 `json:"tgId" gorm:"column:tg_id"`
  301. Comment string `json:"comment"`
  302. Reset int `json:"reset" gorm:"default:0"`
  303. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  304. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  305. }
  306. func (ClientRecord) TableName() string { return "clients" }
  307. // MarshalJSON emits the reverse column as a nested JSON object rather than an
  308. // escaped JSON-text string, matching the same convention Inbound uses for its
  309. // JSON-text columns. Empty storage renders as null.
  310. func (r ClientRecord) MarshalJSON() ([]byte, error) {
  311. type alias ClientRecord
  312. return json.Marshal(struct {
  313. alias
  314. Reverse json.RawMessage `json:"reverse"`
  315. }{
  316. alias: alias(r),
  317. Reverse: jsonStringFieldToRaw(r.Reverse),
  318. })
  319. }
  320. // UnmarshalJSON accepts reverse as either a JSON object (modern shape) or a
  321. // JSON-encoded string (legacy shape).
  322. func (r *ClientRecord) UnmarshalJSON(data []byte) error {
  323. type alias ClientRecord
  324. aux := struct {
  325. *alias
  326. Reverse json.RawMessage `json:"reverse"`
  327. }{
  328. alias: (*alias)(r),
  329. }
  330. if err := json.Unmarshal(data, &aux); err != nil {
  331. return err
  332. }
  333. r.Reverse = jsonStringFieldFromRaw(aux.Reverse)
  334. return nil
  335. }
  336. type ClientInbound struct {
  337. ClientId int `json:"clientId" gorm:"primaryKey;column:client_id;index"`
  338. InboundId int `json:"inboundId" gorm:"primaryKey;column:inbound_id;index"`
  339. FlowOverride string `json:"flowOverride" gorm:"column:flow_override"`
  340. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  341. }
  342. func (ClientInbound) TableName() string { return "client_inbounds" }
  343. // InboundFallback is one routing rule on a master inbound's
  344. // settings.fallbacks array. The master is always a VLESS or Trojan
  345. // inbound on TCP transport with TLS or Reality. The child is any other
  346. // inbound — its listen+port becomes the fallback dest, with optional
  347. // SNI/ALPN/path match criteria pulled from the same row.
  348. type InboundFallback struct {
  349. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  350. MasterId int `json:"masterId" gorm:"index;not null;column:master_id"`
  351. ChildId int `json:"childId" gorm:"index;not null;column:child_id"`
  352. Name string `json:"name"`
  353. Alpn string `json:"alpn"`
  354. Path string `json:"path"`
  355. Xver int `json:"xver"`
  356. SortOrder int `json:"sortOrder" gorm:"default:0;column:sort_order"`
  357. }
  358. func (InboundFallback) TableName() string { return "inbound_fallbacks" }
  359. func (c *Client) ToRecord() *ClientRecord {
  360. rec := &ClientRecord{
  361. Email: c.Email,
  362. SubID: c.SubID,
  363. UUID: c.ID,
  364. Password: c.Password,
  365. Auth: c.Auth,
  366. Flow: c.Flow,
  367. Security: c.Security,
  368. LimitIP: c.LimitIP,
  369. TotalGB: c.TotalGB,
  370. ExpiryTime: c.ExpiryTime,
  371. Enable: c.Enable,
  372. TgID: c.TgID,
  373. Comment: c.Comment,
  374. Reset: c.Reset,
  375. CreatedAt: c.CreatedAt,
  376. UpdatedAt: c.UpdatedAt,
  377. }
  378. if c.Reverse != nil {
  379. if b, err := json.Marshal(c.Reverse); err == nil {
  380. rec.Reverse = string(b)
  381. }
  382. }
  383. return rec
  384. }
  385. func (r *ClientRecord) ToClient() *Client {
  386. c := &Client{
  387. ID: r.UUID,
  388. Email: r.Email,
  389. SubID: r.SubID,
  390. Password: r.Password,
  391. Auth: r.Auth,
  392. Flow: r.Flow,
  393. Security: r.Security,
  394. LimitIP: r.LimitIP,
  395. TotalGB: r.TotalGB,
  396. ExpiryTime: r.ExpiryTime,
  397. Enable: r.Enable,
  398. TgID: r.TgID,
  399. Comment: r.Comment,
  400. Reset: r.Reset,
  401. CreatedAt: r.CreatedAt,
  402. UpdatedAt: r.UpdatedAt,
  403. }
  404. if r.Reverse != "" {
  405. var rev ClientReverse
  406. if err := json.Unmarshal([]byte(r.Reverse), &rev); err == nil {
  407. c.Reverse = &rev
  408. }
  409. }
  410. return c
  411. }
  412. type ClientMergeConflict struct {
  413. Field string
  414. Old any
  415. New any
  416. Kept any
  417. }
  418. func MergeClientRecord(existing *ClientRecord, incoming *ClientRecord) []ClientMergeConflict {
  419. var conflicts []ClientMergeConflict
  420. keep := func(field string, oldV, newV, kept any) {
  421. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: oldV, New: newV, Kept: kept})
  422. }
  423. const redacted = "<redacted>"
  424. keepSecret := func(field string) {
  425. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: redacted, New: redacted, Kept: redacted})
  426. }
  427. incomingNewer := incoming.UpdatedAt > existing.UpdatedAt ||
  428. (incoming.UpdatedAt == existing.UpdatedAt && incoming.CreatedAt > existing.CreatedAt)
  429. if existing.UUID != incoming.UUID && incoming.UUID != "" {
  430. if incomingNewer || existing.UUID == "" {
  431. existing.UUID = incoming.UUID
  432. }
  433. keepSecret("uuid")
  434. }
  435. if existing.Password != incoming.Password && incoming.Password != "" {
  436. if incomingNewer || existing.Password == "" {
  437. existing.Password = incoming.Password
  438. keepSecret("password")
  439. }
  440. }
  441. if existing.Auth != incoming.Auth && incoming.Auth != "" {
  442. if incomingNewer || existing.Auth == "" {
  443. existing.Auth = incoming.Auth
  444. keepSecret("auth")
  445. }
  446. }
  447. if existing.Flow != incoming.Flow && incoming.Flow != "" {
  448. if incomingNewer || existing.Flow == "" {
  449. keep("flow", existing.Flow, incoming.Flow, incoming.Flow)
  450. existing.Flow = incoming.Flow
  451. }
  452. }
  453. if existing.Security != incoming.Security && incoming.Security != "" {
  454. if incomingNewer || existing.Security == "" {
  455. keep("security", existing.Security, incoming.Security, incoming.Security)
  456. existing.Security = incoming.Security
  457. }
  458. }
  459. if existing.SubID != incoming.SubID && incoming.SubID != "" {
  460. if incomingNewer || existing.SubID == "" {
  461. existing.SubID = incoming.SubID
  462. keepSecret("subId")
  463. }
  464. }
  465. if existing.TotalGB != incoming.TotalGB {
  466. picked := existing.TotalGB
  467. if existing.TotalGB == 0 || (incoming.TotalGB != 0 && incoming.TotalGB > existing.TotalGB) {
  468. picked = incoming.TotalGB
  469. }
  470. if picked != existing.TotalGB {
  471. keep("totalGB", existing.TotalGB, incoming.TotalGB, picked)
  472. existing.TotalGB = picked
  473. }
  474. }
  475. if existing.ExpiryTime != incoming.ExpiryTime {
  476. picked := existing.ExpiryTime
  477. if existing.ExpiryTime == 0 || (incoming.ExpiryTime != 0 && incoming.ExpiryTime > existing.ExpiryTime) {
  478. picked = incoming.ExpiryTime
  479. }
  480. if picked != existing.ExpiryTime {
  481. keep("expiryTime", existing.ExpiryTime, incoming.ExpiryTime, picked)
  482. existing.ExpiryTime = picked
  483. }
  484. }
  485. if existing.LimitIP != incoming.LimitIP && incoming.LimitIP != 0 {
  486. picked := existing.LimitIP
  487. if existing.LimitIP == 0 || incoming.LimitIP > existing.LimitIP {
  488. picked = incoming.LimitIP
  489. }
  490. if picked != existing.LimitIP {
  491. keep("limitIp", existing.LimitIP, incoming.LimitIP, picked)
  492. existing.LimitIP = picked
  493. }
  494. }
  495. if existing.TgID != incoming.TgID && incoming.TgID != 0 {
  496. if incomingNewer || existing.TgID == 0 {
  497. keep("tgId", existing.TgID, incoming.TgID, incoming.TgID)
  498. existing.TgID = incoming.TgID
  499. }
  500. }
  501. if existing.Reset != incoming.Reset && incoming.Reset != 0 {
  502. if incomingNewer || existing.Reset == 0 {
  503. keep("reset", existing.Reset, incoming.Reset, incoming.Reset)
  504. existing.Reset = incoming.Reset
  505. }
  506. }
  507. if existing.Reverse != incoming.Reverse && incoming.Reverse != "" {
  508. if incomingNewer || existing.Reverse == "" {
  509. keep("reverse", existing.Reverse, incoming.Reverse, incoming.Reverse)
  510. existing.Reverse = incoming.Reverse
  511. }
  512. }
  513. if existing.Comment != incoming.Comment && incoming.Comment != "" {
  514. if incomingNewer || existing.Comment == "" {
  515. keep("comment", existing.Comment, incoming.Comment, incoming.Comment)
  516. existing.Comment = incoming.Comment
  517. }
  518. }
  519. if existing.Enable != incoming.Enable {
  520. if incoming.Enable {
  521. if !existing.Enable {
  522. keep("enable", existing.Enable, incoming.Enable, true)
  523. existing.Enable = true
  524. }
  525. }
  526. }
  527. if incoming.CreatedAt != 0 && (existing.CreatedAt == 0 || incoming.CreatedAt < existing.CreatedAt) {
  528. existing.CreatedAt = incoming.CreatedAt
  529. }
  530. if incoming.UpdatedAt > existing.UpdatedAt {
  531. existing.UpdatedAt = incoming.UpdatedAt
  532. }
  533. return conflicts
  534. }