浏览代码

chore(build): bump Go toolchain to 1.27.0

Go 1.27.0 shipped on 2026-08-19. Raise the go directive and the builder
image so Docker and release builds pick it up; every CI job already reads
the version from go.mod, and golangci-lint v2.13.1 release binaries are
themselves built with go1.27.0, so the lint job needs no pin change.
Sanaei 8 小时之前
父节点
当前提交
81fcacab11

+ 1 - 1
.github/claude/repo-context.md

@@ -15,7 +15,7 @@ question it already answers.
 
 
 3x-ui is an open-source web control panel for managing Xray-core servers.
 3x-ui is an open-source web control panel for managing Xray-core servers.
 
 
-- Backend: Go 1.26, module `github.com/mhsanaei/3x-ui/v3`, Gin and GORM.
+- Backend: Go 1.27, module `github.com/mhsanaei/3x-ui/v3`, Gin and GORM.
 - It runs Xray-core as a managed child process (`internal/xray/process.go`) and
 - It runs Xray-core as a managed child process (`internal/xray/process.go`) and
   imports `github.com/xtls/xray-core` for config types and the gRPC
   imports `github.com/xtls/xray-core` for config types and the gRPC
   stats/handler/router API. The release the panel BUNDLES is pinned in
   stats/handler/router API. The release the panel BUNDLES is pinned in

+ 1 - 1
CLAUDE.md

@@ -8,7 +8,7 @@ index, layering rules), read `docs/architecture.md` on demand — do not guess
 file locations when it can answer in one hop.
 file locations when it can answer in one hop.
 
 
 ## Stack
 ## Stack
-- Backend: Go 1.26 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM.
+- Backend: Go 1.27 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM.
   Runs Xray-core as a managed child process (`internal/xray/process.go`) and
   Runs Xray-core as a managed child process (`internal/xray/process.go`) and
   imports `github.com/xtls/xray-core` for config types + gRPC stats/handler/router
   imports `github.com/xtls/xray-core` for config types + gRPC stats/handler/router
   API. MTProto inbounds run a second managed child — the `mtg-multi` binary
   API. MTProto inbounds run a second managed child — the `mtg-multi` binary

+ 1 - 1
CONTRIBUTING.md

@@ -4,7 +4,7 @@ Thanks for taking the time to contribute to 3x-ui. This guide gets a development
 
 
 ## Prerequisites
 ## Prerequisites
 
 
-- **Go 1.26+** (the version pinned in `go.mod`)
+- **Go 1.27+** (the version pinned in `go.mod`)
 - **Node.js 24 LTS** (the version pinned in `.nvmrc`) and npm 10+ (for the React frontend)
 - **Node.js 24 LTS** (the version pinned in `.nvmrc`) and npm 10+ (for the React frontend)
 - **Git**
 - **Git**
 - **A C compiler** — required by the CGo SQLite driver (`github.com/mattn/go-sqlite3`). Linux and macOS already ship one; for Windows see below.
 - **A C compiler** — required by the CGo SQLite driver (`github.com/mattn/go-sqlite3`). Linux and macOS already ship one; for Windows see below.

+ 1 - 1
Dockerfile

@@ -12,7 +12,7 @@ RUN npm run build
 # ========================================================
 # ========================================================
 # Stage: Builder
 # Stage: Builder
 # ========================================================
 # ========================================================
-FROM golang:1.26-alpine AS builder
+FROM golang:1.27-alpine AS builder
 WORKDIR /app
 WORKDIR /app
 ARG TARGETARCH
 ARG TARGETARCH
 
 

+ 2 - 2
bot_context_test.go

@@ -37,8 +37,8 @@ func section(t *testing.T, doc, from, to string) string {
 		t.Fatalf("%s no longer contains the heading %q", botContextPath, from)
 		t.Fatalf("%s no longer contains the heading %q", botContextPath, from)
 	}
 	}
 	rest := doc[i+len(from):]
 	rest := doc[i+len(from):]
-	if j := strings.Index(rest, to); j >= 0 {
-		return rest[:j]
+	if before, _, ok := strings.Cut(rest, to); ok {
+		return before
 	}
 	}
 	return rest
 	return rest
 }
 }

+ 1 - 1
docs/architecture.md

@@ -51,7 +51,7 @@ Two key ideas that explain most of the complexity:
 
 
 ## 2. Tech stack
 ## 2. Tech stack
 
 
-**Backend (Go 1.26):**
+**Backend (Go 1.27):**
 
 
 - Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.
 - Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.
 - ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).
 - ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).

+ 5 - 5
go.mod

@@ -1,6 +1,6 @@
 module github.com/mhsanaei/3x-ui/v3
 module github.com/mhsanaei/3x-ui/v3
 
 
-go 1.26.6
+go 1.27.0
 
 
 require (
 require (
 	github.com/gin-contrib/gzip v1.2.6
 	github.com/gin-contrib/gzip v1.2.6
@@ -13,10 +13,12 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/websocket v1.5.3
 	github.com/gorilla/websocket v1.5.3
 	github.com/joho/godotenv v1.5.1
 	github.com/joho/godotenv v1.5.1
+	github.com/klauspost/compress v1.19.2
 	github.com/mattn/go-sqlite3 v1.14.49
 	github.com/mattn/go-sqlite3 v1.14.49
 	github.com/mymmrac/telego v1.11.1
 	github.com/mymmrac/telego v1.11.1
 	github.com/nicksnyder/go-i18n/v2 v2.6.1
 	github.com/nicksnyder/go-i18n/v2 v2.6.1
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
+	github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/shirou/gopsutil/v4 v4.26.7
 	github.com/shirou/gopsutil/v4 v4.26.7
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
@@ -25,9 +27,11 @@ require (
 	github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc
 	github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc
 	go.uber.org/atomic v1.11.0
 	go.uber.org/atomic v1.11.0
 	golang.org/x/crypto v0.55.0
 	golang.org/x/crypto v0.55.0
+	golang.org/x/net v0.58.0
 	golang.org/x/sys v0.47.0
 	golang.org/x/sys v0.47.0
 	golang.org/x/text v0.41.0
 	golang.org/x/text v0.41.0
 	google.golang.org/grpc v1.83.0
 	google.golang.org/grpc v1.83.0
+	google.golang.org/protobuf v1.36.12
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gorm.io/driver/postgres v1.6.2
 	gorm.io/driver/postgres v1.6.2
 	gorm.io/driver/sqlite v1.6.0
 	gorm.io/driver/sqlite v1.6.0
@@ -64,7 +68,6 @@ require (
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/juju/ratelimit v1.0.2 // indirect
 	github.com/juju/ratelimit v1.0.2 // indirect
-	github.com/klauspost/compress v1.19.2
 	github.com/klauspost/cpuid/v2 v2.4.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.4.0 // indirect
 	github.com/leodido/go-urn v1.5.0 // indirect
 	github.com/leodido/go-urn v1.5.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect
 	github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect
@@ -81,7 +84,6 @@ require (
 	github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
 	github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
 	github.com/quic-go/qpack v0.6.0 // indirect
 	github.com/quic-go/qpack v0.6.0 // indirect
 	github.com/quic-go/quic-go v0.61.0 // indirect
 	github.com/quic-go/quic-go v0.61.0 // indirect
-	github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
 	github.com/rogpeppe/go-internal v1.15.0 // indirect
 	github.com/rogpeppe/go-internal v1.15.0 // indirect
 	github.com/sagernet/sing v0.8.13 // indirect
 	github.com/sagernet/sing v0.8.13 // indirect
 	github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
 	github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
@@ -101,7 +103,6 @@ require (
 	golang.org/x/arch v0.30.0 // indirect
 	golang.org/x/arch v0.30.0 // indirect
 	golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect
 	golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect
 	golang.org/x/mod v0.40.0 // indirect
 	golang.org/x/mod v0.40.0 // indirect
-	golang.org/x/net v0.58.0
 	golang.org/x/sync v0.22.0 // indirect
 	golang.org/x/sync v0.22.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
 	golang.org/x/tools v0.49.0 // indirect
 	golang.org/x/tools v0.49.0 // indirect
@@ -109,7 +110,6 @@ require (
 	golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 // indirect
 	golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
 	golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // indirect
-	google.golang.org/protobuf v1.36.12
 	gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
 	gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
 	lukechampine.com/blake3 v1.4.1 // indirect
 	lukechampine.com/blake3 v1.4.1 // indirect
 )
 )

+ 1 - 1
internal/pia/catalog_test.go

@@ -103,7 +103,7 @@ func TestCatalogCoalescesConcurrentRefresh(t *testing.T) {
 		time.Sleep(time.Millisecond)
 		time.Sleep(time.Millisecond)
 	}
 	}
 	close(source.release)
 	close(source.release)
-	for i := 0; i < 2; i++ {
+	for range 2 {
 		if err := <-errc; err != nil {
 		if err := <-errc; err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
 		}
 		}

+ 1 - 1
internal/pia/validation.go

@@ -23,7 +23,7 @@ func validHostname(host string) bool {
 	if host == "" || len(host) > 253 || net.ParseIP(host) != nil || strings.HasSuffix(host, ".") {
 	if host == "" || len(host) > 253 || net.ParseIP(host) != nil || strings.HasSuffix(host, ".") {
 		return false
 		return false
 	}
 	}
-	for _, label := range strings.Split(host, ".") {
+	for label := range strings.SplitSeq(host, ".") {
 		if label == "" || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' {
 		if label == "" || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' {
 			return false
 			return false
 		}
 		}

+ 9 - 9
internal/sub/json_service.go

@@ -387,10 +387,10 @@ func jsonMux(global, override string) string {
 }
 }
 
 
 func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage {
 func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage {
-	outbound := Outbound{}
+	outbound := Outbound{
 
 
-	outbound.Protocol = string(inbound.Protocol)
-	outbound.Tag = "proxy"
+		Protocol: string(inbound.Protocol),
+		Tag:      "proxy"}
 	if mux != "" {
 	if mux != "" {
 		outbound.Mux = json_util.RawMessage(mux)
 		outbound.Mux = json_util.RawMessage(mux)
 	}
 	}
@@ -410,9 +410,9 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
 }
 }
 
 
 func (s *SubJsonService) genVless(subReq *SubService, inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage {
 func (s *SubJsonService) genVless(subReq *SubService, inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage {
-	outbound := Outbound{}
-	outbound.Protocol = string(inbound.Protocol)
-	outbound.Tag = "proxy"
+	outbound := Outbound{
+		Protocol: string(inbound.Protocol),
+		Tag:      "proxy"}
 	if mux != "" {
 	if mux != "" {
 		outbound.Mux = json_util.RawMessage(mux)
 		outbound.Mux = json_util.RawMessage(mux)
 	}
 	}
@@ -490,10 +490,10 @@ func (s *SubJsonService) genServer(subReq *SubService, inbound *model.Inbound, s
 }
 }
 
 
 func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client, mux string) json_util.RawMessage {
 func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client, mux string) json_util.RawMessage {
-	outbound := Outbound{}
+	outbound := Outbound{
 
 
-	outbound.Protocol = string(inbound.Protocol)
-	outbound.Tag = "proxy"
+		Protocol: string(inbound.Protocol),
+		Tag:      "proxy"}
 
 
 	if mux != "" {
 	if mux != "" {
 		outbound.Mux = json_util.RawMessage(mux)
 		outbound.Mux = json_util.RawMessage(mux)

+ 3 - 2
internal/sub/mutation_audit_test.go

@@ -25,8 +25,9 @@ func initMutDB(t *testing.T) {
 	t.Cleanup(func() { _ = database.CloseDB() })
 	t.Cleanup(func() { _ = database.CloseDB() })
 }
 }
 
 
+//go:fix inline
 func externalLinkEnabled(v bool) *bool {
 func externalLinkEnabled(v bool) *bool {
-	return &v
+	return new(v)
 }
 }
 
 
 // --- json_service.go:40 — rules are merged into routing only when non-empty ---
 // --- json_service.go:40 — rules are merged into routing only when non-empty ---
@@ -312,7 +313,7 @@ func TestGetClientExternalLinksBySubId(t *testing.T) {
 	if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://a", Remark: "first", SortIndex: 1}).Error; err != nil {
 	if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://a", Remark: "first", SortIndex: 1}).Error; err != nil {
 		t.Fatalf("seed link a: %v", err)
 		t.Fatalf("seed link a: %v", err)
 	}
 	}
-	if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://disabled", Remark: "disabled", Enable: externalLinkEnabled(false), SortIndex: 3}).Error; err != nil {
+	if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://disabled", Remark: "disabled", Enable: new(false), SortIndex: 3}).Error; err != nil {
 		t.Fatalf("seed disabled link: %v", err)
 		t.Fatalf("seed disabled link: %v", err)
 	}
 	}
 	if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://expired", Remark: "expired", ExpiryTime: time.Now().Add(-time.Hour).UnixMilli(), SortIndex: 4}).Error; err != nil {
 	if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://expired", Remark: "expired", ExpiryTime: time.Now().Add(-time.Hour).UnixMilli(), SortIndex: 4}).Error; err != nil {

+ 2 - 2
internal/sub/remote_routing.go

@@ -370,8 +370,8 @@ func normalizeHappRouting(body []byte) (string, error) {
 
 
 	payload := ""
 	payload := ""
 	for _, prefix := range []string{"happ://routing/onadd/", "happ://routing/add/"} {
 	for _, prefix := range []string{"happ://routing/onadd/", "happ://routing/add/"} {
-		if strings.HasPrefix(text, prefix) {
-			payload = strings.TrimPrefix(text, prefix)
+		if after, ok := strings.CutPrefix(text, prefix); ok {
+			payload = after
 			break
 			break
 		}
 		}
 	}
 	}

+ 3 - 3
internal/web/controller/api.go

@@ -155,11 +155,11 @@ func (a *APIController) enforceTokenScope(c *gin.Context) {
 
 
 func relAPIPath(fullPath string) string {
 func relAPIPath(fullPath string) string {
 	const marker = "/panel/api"
 	const marker = "/panel/api"
-	i := strings.Index(fullPath, marker)
-	if i < 0 {
+	_, after, ok := strings.Cut(fullPath, marker)
+	if !ok {
 		return ""
 		return ""
 	}
 	}
-	return fullPath[i+len(marker):]
+	return after
 }
 }
 
 
 // initRouter sets up the API routes for inbounds, server, and other endpoints.
 // initRouter sets up the API routes for inbounds, server, and other endpoints.

+ 4 - 5
internal/web/job/ip_limit_allowlist.go

@@ -2,6 +2,7 @@ package job
 
 
 import (
 import (
 	"net/netip"
 	"net/netip"
+	"slices"
 	"strings"
 	"strings"
 )
 )
 
 
@@ -16,7 +17,7 @@ type ipLimitAllowlist struct {
 // skipped: the validator uses these same rules, so only a hand-edited DB differs.
 // skipped: the validator uses these same rules, so only a hand-edited DB differs.
 func parseIpLimitAllowlist(raw string) ipLimitAllowlist {
 func parseIpLimitAllowlist(raw string) ipLimitAllowlist {
 	var list ipLimitAllowlist
 	var list ipLimitAllowlist
-	for _, field := range strings.Split(raw, ",") {
+	for field := range strings.SplitSeq(raw, ",") {
 		field = strings.TrimSpace(field)
 		field = strings.TrimSpace(field)
 		if field == "" {
 		if field == "" {
 			continue
 			continue
@@ -52,10 +53,8 @@ func (l ipLimitAllowlist) contains(ip string) bool {
 		return false
 		return false
 	}
 	}
 	addr = addr.Unmap()
 	addr = addr.Unmap()
-	for _, allowed := range l.addrs {
-		if allowed == addr {
-			return true
-		}
+	if slices.Contains(l.addrs, addr) {
+		return true
 	}
 	}
 	for _, prefix := range l.prefixes {
 	for _, prefix := range l.prefixes {
 		if prefix.Contains(addr) {
 		if prefix.Contains(addr) {

+ 2 - 2
internal/web/network/auto_https_conn.go

@@ -46,8 +46,8 @@ func (c *AutoHttpsConn) readRequest() bool {
 	}
 	}
 	resp := http.Response{
 	resp := http.Response{
 		Header: http.Header{},
 		Header: http.Header{},
-	}
-	resp.StatusCode = http.StatusTemporaryRedirect
+
+		StatusCode: http.StatusTemporaryRedirect}
 	location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
 	location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
 	resp.Header.Set("Location", location)
 	resp.Header.Set("Location", location)
 	_ = resp.Write(c.Conn)
 	_ = resp.Write(c.Conn)

+ 3 - 7
internal/web/service/client_external_link_test.go

@@ -7,10 +7,6 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 )
 
 
-func externalLinkBool(v bool) *bool {
-	return &v
-}
-
 func TestSetExternalLinksPersistsEnableState(t *testing.T) {
 func TestSetExternalLinksPersistsEnableState(t *testing.T) {
 	setupBulkDB(t)
 	setupBulkDB(t)
 	db := database.GetDB()
 	db := database.GetDB()
@@ -22,8 +18,8 @@ func TestSetExternalLinksPersistsEnableState(t *testing.T) {
 	}
 	}
 
 
 	if err := svc.SetExternalLinksForRecord(rec.Id, []ExternalLinkInput{
 	if err := svc.SetExternalLinksForRecord(rec.Id, []ExternalLinkInput{
-		{Kind: model.ExternalLinkKindLink, Value: "trojan://[email protected]:443#on", Remark: "Primary", Enable: externalLinkBool(true), ExpiryTime: 1767225600000},
-		{Kind: model.ExternalLinkKindSubscription, Value: "https://provider.example/sub", Remark: "Provider", Enable: externalLinkBool(false), NamePrefix: "[zjh] "},
+		{Kind: model.ExternalLinkKindLink, Value: "trojan://[email protected]:443#on", Remark: "Primary", Enable: new(true), ExpiryTime: 1767225600000},
+		{Kind: model.ExternalLinkKindSubscription, Value: "https://provider.example/sub", Remark: "Provider", Enable: new(false), NamePrefix: "[zjh] "},
 		{Kind: model.ExternalLinkKindLink, Value: "trojan://[email protected]:443#default"},
 		{Kind: model.ExternalLinkKindLink, Value: "trojan://[email protected]:443#default"},
 	}); err != nil {
 	}); err != nil {
 		t.Fatalf("set external links: %v", err)
 		t.Fatalf("set external links: %v", err)
@@ -76,7 +72,7 @@ func TestSetExternalLinksPreservesFetchStatus(t *testing.T) {
 	}
 	}
 
 
 	if err := svc.SetExternalLinksForRecord(rec.Id, []ExternalLinkInput{
 	if err := svc.SetExternalLinksForRecord(rec.Id, []ExternalLinkInput{
-		{Kind: row.Kind, Value: row.Value, Remark: "new", Enable: externalLinkBool(true)},
+		{Kind: row.Kind, Value: row.Value, Remark: "new", Enable: new(true)},
 	}); err != nil {
 	}); err != nil {
 		t.Fatalf("set external links: %v", err)
 		t.Fatalf("set external links: %v", err)
 	}
 	}

+ 5 - 6
internal/web/service/client_test.go

@@ -4,15 +4,14 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"testing"
 	"testing"
 
 
-	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 )
 )
 
 
 func TestClientWithAttachmentsMarshalJSONIncludesExtras(t *testing.T) {
 func TestClientWithAttachmentsMarshalJSONIncludesExtras(t *testing.T) {
 	c := ClientWithAttachments{
 	c := ClientWithAttachments{
-		ClientRecord: model.ClientRecord{Id: 1, Email: "[email protected]"},
-		InboundIds:   []int{3, 5},
-		Traffic:      &xray.ClientTraffic{Email: "[email protected]", Up: 1024, Down: 4096, Enable: true},
+		Id: 1, Email: "[email protected]",
+		InboundIds: []int{3, 5},
+		Traffic:    &xray.ClientTraffic{Email: "[email protected]", Up: 1024, Down: 4096, Enable: true},
 	}
 	}
 	out, err := json.Marshal(c)
 	out, err := json.Marshal(c)
 	if err != nil {
 	if err != nil {
@@ -39,8 +38,8 @@ func TestClientWithAttachmentsMarshalJSONIncludesExtras(t *testing.T) {
 
 
 func TestClientWithAttachmentsMarshalJSONOmitsAbsentTraffic(t *testing.T) {
 func TestClientWithAttachmentsMarshalJSONOmitsAbsentTraffic(t *testing.T) {
 	c := ClientWithAttachments{
 	c := ClientWithAttachments{
-		ClientRecord: model.ClientRecord{Id: 1, Email: "[email protected]"},
-		InboundIds:   nil,
+		Id: 1, Email: "[email protected]",
+		InboundIds: nil,
 	}
 	}
 	out, err := json.Marshal(c)
 	out, err := json.Marshal(c)
 	if err != nil {
 	if err != nil {

+ 1 - 1
internal/web/service/inbound_autorenew_calendar_test.go

@@ -118,7 +118,7 @@ func TestAutoRenewClients_CalendarModeClampsShortMonths(t *testing.T) {
 func firstBillingMidnightAfter(t *testing.T, from time.Time, day int, loc *time.Location) time.Time {
 func firstBillingMidnightAfter(t *testing.T, from time.Time, day int, loc *time.Location) time.Time {
 	t.Helper()
 	t.Helper()
 	cur := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, loc)
 	cur := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, loc)
-	for i := 0; i < 400; i++ {
+	for range 400 {
 		cur = cur.AddDate(0, 0, 1)
 		cur = cur.AddDate(0, 0, 1)
 		want := day
 		want := day
 		if last := time.Date(cur.Year(), cur.Month()+1, 0, 0, 0, 0, 0, loc).Day(); want > last {
 		if last := time.Date(cur.Year(), cur.Month()+1, 0, 0, 0, 0, 0, loc).Day(); want > last {

+ 1 - 1
internal/web/service/setting_mtls_test.go

@@ -187,7 +187,7 @@ func TestEnsureMasterClientCertConcurrentFirstUseMintsOneCredential(t *testing.T
 	}
 	}
 	close(start)
 	close(start)
 	var first crypto.CertKeyPEM
 	var first crypto.CertKeyPEM
-	for i := 0; i < callers; i++ {
+	for i := range callers {
 		credential := <-results
 		credential := <-results
 		if err := <-errs; err != nil {
 		if err := <-errs; err != nil {
 			t.Fatalf("caller %d: %v", i, err)
 			t.Fatalf("caller %d: %v", i, err)

+ 1 - 4
internal/xray/geodata/query.go

@@ -103,10 +103,7 @@ func sliceBounds(total, offset, limit int) (int, int) {
 	if limit <= 0 || limit > MaxPageSize {
 	if limit <= 0 || limit > MaxPageSize {
 		limit = MaxPageSize
 		limit = MaxPageSize
 	}
 	}
-	to := offset + limit
-	if to > total {
-		to = total
-	}
+	to := min(offset+limit, total)
 	return offset, to
 	return offset, to
 }
 }