From cbe258fd1284f1286cdc1c3843d9958a6f49dad2 Mon Sep 17 00:00:00 2001 From: feeeei Date: Sun, 23 Aug 2026 20:41:45 +0800 Subject: [PATCH 1/3] =?UTF-8?q?build:=20=E5=8D=87=E7=BA=A7=20Go=201.27.0?= =?UTF-8?q?=EF=BC=8C=E5=90=8C=E6=AD=A5=20CI/Dockerfile=20=E5=B9=B6?= =?UTF-8?q?=E9=80=82=E9=85=8D=20jsonv2=20=E4=B8=8E=20golangci-lint=20v2.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - go.mod 1.26.6 → 1.27.0;backend-ci/release/security-scan 的 go version 断言、 三个 Dockerfile 的 golang 镜像、README 徽章与 DEV_GUIDE 同步 - golangci-lint-action v2.9 → v2.13(v2.9 由 go1.26 构建,拒绝 go.mod 1.27 目标); 新规则按最小方式处理:排除 G703/G704 污点分析(网关按配置转发/写文件, 与既有 G304 排除策略一致)、reflect.Ptr → reflect.Pointer、 ResetQuota 恒返回错误的 SA4023 与 OIDC EC JWK 的 SA1019 加 nolint - ent 生成代码按 Go 1.27 默认 jsonv2 引擎重新生成:json.RawMessage 字段 生成为同类型别名 jsontext.Value(group.model_pricing / usage_cleanup_task.filters) - x/net v0.56 在 go1.27 下包装标准库 HTTP/2:ConfigureTransports 经 RegisterProtocol("http/2") 打开 Protocols.HTTP2 而不再写 TLSNextProto, ReadIdleTimeout/PingTimeout 建连时映射为 HTTP2Config.SendPingTimeout/PingTimeout; keepalive 测试改断言 Protocols.HTTP2(),并补真实 HTTP/2 协商用例 --- .github/workflows/backend-ci.yml | 6 +-- .github/workflows/release.yml | 2 +- .github/workflows/security-scan.yml | 2 +- DEV_GUIDE.md | 4 +- Dockerfile | 2 +- README.md | 4 +- README_CN.md | 4 +- README_JA.md | 4 +- backend/.golangci.yml | 4 ++ backend/Dockerfile | 2 +- backend/ent/group.go | 3 +- backend/ent/group_create.go | 10 ++-- backend/ent/group_update.go | 10 ++-- backend/ent/mutation.go | 46 ++++++++--------- backend/ent/usagecleanuptask.go | 3 +- backend/ent/usagecleanuptask_create.go | 10 ++-- backend/ent/usagecleanuptask_update.go | 10 ++-- backend/go.mod | 2 +- .../handler/admin/grok_oauth_handler.go | 4 +- .../handler/admin/setting_handler_update.go | 2 +- backend/internal/handler/auth_oidc_oauth.go | 4 +- .../http_upstream_http2_keepalive_test.go | 49 +++++++++++++++++-- .../usage_log_repo_request_type_test.go | 2 +- deploy/Dockerfile | 2 +- 24 files changed, 120 insertions(+), 71 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index ece09c5dc1..2bac7259a9 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -33,7 +33,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.6' + go version | grep -q 'go1.27.0' - name: Unit tests working-directory: backend run: make test-unit @@ -73,10 +73,10 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.6' + go version | grep -q 'go1.27.0' - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.9 + version: v2.13 args: --timeout=30m working-directory: backend diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ecc8a26ba..abdfc9c81d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,7 +115,7 @@ jobs: - name: Verify Go version run: | - go version | grep -q 'go1.26.6' + go version | grep -q 'go1.27.0' # Docker setup for GoReleaser - name: Set up QEMU diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 0e68d950ac..97357efde2 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -23,7 +23,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.6' + go version | grep -q 'go1.27.0' - name: Run govulncheck working-directory: backend run: | diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index bf5e8867d6..cb1ffabdb3 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -53,7 +53,7 @@ npm install -g pnpm ### CI 要求 -- Go 版本必须是 **1.26.6**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.26.6'`。升级 Go 时要同时改 `backend/go.mod`、`backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,**以及三个 Dockerfile 里的 Go 构建镜像**(`Dockerfile` / `deploy/Dockerfile` 的 `ARG GOLANG_IMAGE`、`backend/Dockerfile` 的 `FROM golang:`)。前者漏了 CI 会在版本校验步骤直接失败;**后者漏了 CI 不会报,而是等到有人用这些 Dockerfile 构建时才失败**(`go.mod requires go >= X (running Y; GOTOOLCHAIN=local)`)。 +- Go 版本必须是 **1.27.0**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.27.0'`。升级 Go 时要同时改 `backend/go.mod`、`backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,**以及三个 Dockerfile 里的 Go 构建镜像**(`Dockerfile` / `deploy/Dockerfile` 的 `ARG GOLANG_IMAGE`、`backend/Dockerfile` 的 `FROM golang:`)。前者漏了 CI 会在版本校验步骤直接失败;**后者漏了 CI 不会报,而是等到有人用这些 Dockerfile 构建时才失败**(`go.mod requires go >= X (running Y; GOTOOLCHAIN=local)`)。 - 前端使用 `pnpm install --frozen-lockfile`,必须提交 `pnpm-lock.yaml` ### 本地测试命令 @@ -203,7 +203,7 @@ go test -tags=integration ./... **解决**: ```bash cd backend -go generate ./ent # 重新生成 ent 代码 +go generate ./ent # 重新生成 ent 代码(json.RawMessage 字段会生成为同类型的 jsontext.Value,属预期) git add ent/ # 生成的文件也要提交 ``` diff --git a/Dockerfile b/Dockerfile index c7991c5a60..adfa4d4702 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ # ============================================================================= ARG NODE_IMAGE=node:24-alpine -ARG GOLANG_IMAGE=golang:1.26.6-alpine +ARG GOLANG_IMAGE=golang:1.27.0-alpine ARG ALPINE_IMAGE=alpine:3.21 ARG POSTGRES_IMAGE=postgres:18-alpine ARG GOPROXY=https://goproxy.cn,direct diff --git a/README.md b/README.md index 50ffe7e430..e5d3221c1c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Sub2API -[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/) +[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/) [![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) [![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) @@ -194,7 +194,7 @@ Community projects that extend or integrate with Sub2API: | Component | Technology | |-----------|------------| -| Backend | Go 1.26.5, Gin, Ent | +| Backend | Go 1.27.0, Gin, Ent | | Frontend | Vue 3.4+, Vite 5+, TailwindCSS | | Database | PostgreSQL 15+ | | Cache/Queue | Redis 7+ | diff --git a/README_CN.md b/README_CN.md index 034a3819f5..10329d4d8f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -4,7 +4,7 @@ # Sub2API -[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/) +[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/) [![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) [![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) @@ -196,7 +196,7 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的 | 组件 | 技术 | |------|------| -| 后端 | Go 1.26.5, Gin, Ent | +| 后端 | Go 1.27.0, Gin, Ent | | 前端 | Vue 3.4+, Vite 5+, TailwindCSS | | 数据库 | PostgreSQL 15+ | | 缓存/队列 | Redis 7+ | diff --git a/README_JA.md b/README_JA.md index 384f9daf67..8727367980 100644 --- a/README_JA.md +++ b/README_JA.md @@ -4,7 +4,7 @@ # Sub2API -[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/) +[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/) [![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) [![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) @@ -195,7 +195,7 @@ Sub2API を拡張・統合するコミュニティプロジェクト: | コンポーネント | 技術 | |-----------|------------| -| バックエンド | Go 1.26.5, Gin, Ent | +| バックエンド | Go 1.27.0, Gin, Ent | | フロントエンド | Vue 3.4+, Vite 5+, TailwindCSS | | データベース | PostgreSQL 15+ | | キャッシュ/キュー | Redis 7+ | diff --git a/backend/.golangci.yml b/backend/.golangci.yml index 92ba391694..a4aed11470 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -57,6 +57,10 @@ linters: - G304 - G306 - G404 + # G703/G704 是 G304/G107 的污点分析版:网关本职就是按配置转发到上游 URL、按配置写本地文件, + # 污点来源即配置本身,规则不适用。 + - G703 + - G704 severity: high confidence: high errcheck: diff --git a/backend/Dockerfile b/backend/Dockerfile index 1efbd3859d..398d9bdfcf 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.26.6-alpine +FROM golang:1.27.0-alpine WORKDIR /app diff --git a/backend/ent/group.go b/backend/ent/group.go index c9795fb58f..8f30c26470 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -4,6 +4,7 @@ package ent import ( "encoding/json" + "encoding/json/jsontext" "fmt" "strings" "time" @@ -100,7 +101,7 @@ type Group struct { // 是否按上下文长度应用模型阶梯价格;默认开启以保持官方/渠道长上下文价 LongContextPricingEnabled bool `json:"long_context_pricing_enabled,omitempty"` // 分组逐模型定价;优先级高于渠道和内置定价 - ModelPricing json.RawMessage `json:"model_pricing,omitempty"` + ModelPricing jsontext.Value `json:"model_pricing,omitempty"` // 是否仅允许 Claude Code 客户端 ClaudeCodeOnly bool `json:"claude_code_only,omitempty"` // 非 Claude Code 请求降级使用的分组 ID diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index 158dca1e40..e5e92ccd3f 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "time" @@ -575,7 +575,7 @@ func (_c *GroupCreate) SetNillableLongContextPricingEnabled(v *bool) *GroupCreat } // SetModelPricing sets the "model_pricing" field. -func (_c *GroupCreate) SetModelPricing(v json.RawMessage) *GroupCreate { +func (_c *GroupCreate) SetModelPricing(v jsontext.Value) *GroupCreate { _c.mutation.SetModelPricing(v) return _c } @@ -2445,7 +2445,7 @@ func (u *GroupUpsert) UpdateLongContextPricingEnabled() *GroupUpsert { } // SetModelPricing sets the "model_pricing" field. -func (u *GroupUpsert) SetModelPricing(v json.RawMessage) *GroupUpsert { +func (u *GroupUpsert) SetModelPricing(v jsontext.Value) *GroupUpsert { u.Set(group.FieldModelPricing, v) return u } @@ -3615,7 +3615,7 @@ func (u *GroupUpsertOne) UpdateLongContextPricingEnabled() *GroupUpsertOne { } // SetModelPricing sets the "model_pricing" field. -func (u *GroupUpsertOne) SetModelPricing(v json.RawMessage) *GroupUpsertOne { +func (u *GroupUpsertOne) SetModelPricing(v jsontext.Value) *GroupUpsertOne { return u.Update(func(s *GroupUpsert) { s.SetModelPricing(v) }) @@ -5005,7 +5005,7 @@ func (u *GroupUpsertBulk) UpdateLongContextPricingEnabled() *GroupUpsertBulk { } // SetModelPricing sets the "model_pricing" field. -func (u *GroupUpsertBulk) SetModelPricing(v json.RawMessage) *GroupUpsertBulk { +func (u *GroupUpsertBulk) SetModelPricing(v jsontext.Value) *GroupUpsertBulk { return u.Update(func(s *GroupUpsert) { s.SetModelPricing(v) }) diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index beca0c71f0..41e06b4471 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "time" @@ -803,13 +803,13 @@ func (_u *GroupUpdate) SetNillableLongContextPricingEnabled(v *bool) *GroupUpdat } // SetModelPricing sets the "model_pricing" field. -func (_u *GroupUpdate) SetModelPricing(v json.RawMessage) *GroupUpdate { +func (_u *GroupUpdate) SetModelPricing(v jsontext.Value) *GroupUpdate { _u.mutation.SetModelPricing(v) return _u } // AppendModelPricing appends value to the "model_pricing" field. -func (_u *GroupUpdate) AppendModelPricing(v json.RawMessage) *GroupUpdate { +func (_u *GroupUpdate) AppendModelPricing(v jsontext.Value) *GroupUpdate { _u.mutation.AppendModelPricing(v) return _u } @@ -2924,13 +2924,13 @@ func (_u *GroupUpdateOne) SetNillableLongContextPricingEnabled(v *bool) *GroupUp } // SetModelPricing sets the "model_pricing" field. -func (_u *GroupUpdateOne) SetModelPricing(v json.RawMessage) *GroupUpdateOne { +func (_u *GroupUpdateOne) SetModelPricing(v jsontext.Value) *GroupUpdateOne { _u.mutation.SetModelPricing(v) return _u } // AppendModelPricing appends value to the "model_pricing" field. -func (_u *GroupUpdateOne) AppendModelPricing(v json.RawMessage) *GroupUpdateOne { +func (_u *GroupUpdateOne) AppendModelPricing(v jsontext.Value) *GroupUpdateOne { _u.mutation.AppendModelPricing(v) return _u } diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index ddf50a1c42..f777a0a5fb 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "sync" @@ -22142,8 +22142,8 @@ type GroupMutation struct { audio_stt_price_per_hour *float64 addaudio_stt_price_per_hour *float64 long_context_pricing_enabled *bool - model_pricing *json.RawMessage - appendmodel_pricing json.RawMessage + model_pricing *jsontext.Value + appendmodel_pricing jsontext.Value claude_code_only *bool fallback_group_id *int64 addfallback_group_id *int64 @@ -24404,13 +24404,13 @@ func (m *GroupMutation) ResetLongContextPricingEnabled() { } // SetModelPricing sets the "model_pricing" field. -func (m *GroupMutation) SetModelPricing(jm json.RawMessage) { - m.model_pricing = &jm +func (m *GroupMutation) SetModelPricing(j jsontext.Value) { + m.model_pricing = &j m.appendmodel_pricing = nil } // ModelPricing returns the value of the "model_pricing" field in the mutation. -func (m *GroupMutation) ModelPricing() (r json.RawMessage, exists bool) { +func (m *GroupMutation) ModelPricing() (r jsontext.Value, exists bool) { v := m.model_pricing if v == nil { return @@ -24421,7 +24421,7 @@ func (m *GroupMutation) ModelPricing() (r json.RawMessage, exists bool) { // OldModelPricing returns the old "model_pricing" field's value of the Group entity. // If the Group object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldModelPricing(ctx context.Context) (v json.RawMessage, err error) { +func (m *GroupMutation) OldModelPricing(ctx context.Context) (v jsontext.Value, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldModelPricing is only allowed on UpdateOne operations") } @@ -24435,13 +24435,13 @@ func (m *GroupMutation) OldModelPricing(ctx context.Context) (v json.RawMessage, return oldValue.ModelPricing, nil } -// AppendModelPricing adds jm to the "model_pricing" field. -func (m *GroupMutation) AppendModelPricing(jm json.RawMessage) { - m.appendmodel_pricing = append(m.appendmodel_pricing, jm...) +// AppendModelPricing adds j to the "model_pricing" field. +func (m *GroupMutation) AppendModelPricing(j jsontext.Value) { + m.appendmodel_pricing = append(m.appendmodel_pricing, j...) } // AppendedModelPricing returns the list of values that were appended to the "model_pricing" field in this mutation. -func (m *GroupMutation) AppendedModelPricing() (json.RawMessage, bool) { +func (m *GroupMutation) AppendedModelPricing() (jsontext.Value, bool) { if len(m.appendmodel_pricing) == 0 { return nil, false } @@ -26515,7 +26515,7 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { m.SetLongContextPricingEnabled(v) return nil case group.FieldModelPricing: - v, ok := value.(json.RawMessage) + v, ok := value.(jsontext.Value) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } @@ -43151,8 +43151,8 @@ type UsageCleanupTaskMutation struct { created_at *time.Time updated_at *time.Time status *string - filters *json.RawMessage - appendfilters json.RawMessage + filters *jsontext.Value + appendfilters jsontext.Value created_by *int64 addcreated_by *int64 deleted_rows *int64 @@ -43376,13 +43376,13 @@ func (m *UsageCleanupTaskMutation) ResetStatus() { } // SetFilters sets the "filters" field. -func (m *UsageCleanupTaskMutation) SetFilters(jm json.RawMessage) { - m.filters = &jm +func (m *UsageCleanupTaskMutation) SetFilters(j jsontext.Value) { + m.filters = &j m.appendfilters = nil } // Filters returns the value of the "filters" field in the mutation. -func (m *UsageCleanupTaskMutation) Filters() (r json.RawMessage, exists bool) { +func (m *UsageCleanupTaskMutation) Filters() (r jsontext.Value, exists bool) { v := m.filters if v == nil { return @@ -43393,7 +43393,7 @@ func (m *UsageCleanupTaskMutation) Filters() (r json.RawMessage, exists bool) { // OldFilters returns the old "filters" field's value of the UsageCleanupTask entity. // If the UsageCleanupTask object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageCleanupTaskMutation) OldFilters(ctx context.Context) (v json.RawMessage, err error) { +func (m *UsageCleanupTaskMutation) OldFilters(ctx context.Context) (v jsontext.Value, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldFilters is only allowed on UpdateOne operations") } @@ -43407,13 +43407,13 @@ func (m *UsageCleanupTaskMutation) OldFilters(ctx context.Context) (v json.RawMe return oldValue.Filters, nil } -// AppendFilters adds jm to the "filters" field. -func (m *UsageCleanupTaskMutation) AppendFilters(jm json.RawMessage) { - m.appendfilters = append(m.appendfilters, jm...) +// AppendFilters adds j to the "filters" field. +func (m *UsageCleanupTaskMutation) AppendFilters(j jsontext.Value) { + m.appendfilters = append(m.appendfilters, j...) } // AppendedFilters returns the list of values that were appended to the "filters" field in this mutation. -func (m *UsageCleanupTaskMutation) AppendedFilters() (json.RawMessage, bool) { +func (m *UsageCleanupTaskMutation) AppendedFilters() (jsontext.Value, bool) { if len(m.appendfilters) == 0 { return nil, false } @@ -43964,7 +43964,7 @@ func (m *UsageCleanupTaskMutation) SetField(name string, value ent.Value) error m.SetStatus(v) return nil case usagecleanuptask.FieldFilters: - v, ok := value.(json.RawMessage) + v, ok := value.(jsontext.Value) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } diff --git a/backend/ent/usagecleanuptask.go b/backend/ent/usagecleanuptask.go index e3a17b5aed..b4dbf0fd34 100644 --- a/backend/ent/usagecleanuptask.go +++ b/backend/ent/usagecleanuptask.go @@ -4,6 +4,7 @@ package ent import ( "encoding/json" + "encoding/json/jsontext" "fmt" "strings" "time" @@ -25,7 +26,7 @@ type UsageCleanupTask struct { // Status holds the value of the "status" field. Status string `json:"status,omitempty"` // Filters holds the value of the "filters" field. - Filters json.RawMessage `json:"filters,omitempty"` + Filters jsontext.Value `json:"filters,omitempty"` // CreatedBy holds the value of the "created_by" field. CreatedBy int64 `json:"created_by,omitempty"` // DeletedRows holds the value of the "deleted_rows" field. diff --git a/backend/ent/usagecleanuptask_create.go b/backend/ent/usagecleanuptask_create.go index 0b1dcff55e..b6dbcb3cc8 100644 --- a/backend/ent/usagecleanuptask_create.go +++ b/backend/ent/usagecleanuptask_create.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "time" @@ -58,7 +58,7 @@ func (_c *UsageCleanupTaskCreate) SetStatus(v string) *UsageCleanupTaskCreate { } // SetFilters sets the "filters" field. -func (_c *UsageCleanupTaskCreate) SetFilters(v json.RawMessage) *UsageCleanupTaskCreate { +func (_c *UsageCleanupTaskCreate) SetFilters(v jsontext.Value) *UsageCleanupTaskCreate { _c.mutation.SetFilters(v) return _c } @@ -375,7 +375,7 @@ func (u *UsageCleanupTaskUpsert) UpdateStatus() *UsageCleanupTaskUpsert { } // SetFilters sets the "filters" field. -func (u *UsageCleanupTaskUpsert) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsert { +func (u *UsageCleanupTaskUpsert) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsert { u.Set(usagecleanuptask.FieldFilters, v) return u } @@ -592,7 +592,7 @@ func (u *UsageCleanupTaskUpsertOne) UpdateStatus() *UsageCleanupTaskUpsertOne { } // SetFilters sets the "filters" field. -func (u *UsageCleanupTaskUpsertOne) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsertOne { +func (u *UsageCleanupTaskUpsertOne) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsertOne { return u.Update(func(s *UsageCleanupTaskUpsert) { s.SetFilters(v) }) @@ -999,7 +999,7 @@ func (u *UsageCleanupTaskUpsertBulk) UpdateStatus() *UsageCleanupTaskUpsertBulk } // SetFilters sets the "filters" field. -func (u *UsageCleanupTaskUpsertBulk) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsertBulk { +func (u *UsageCleanupTaskUpsertBulk) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsertBulk { return u.Update(func(s *UsageCleanupTaskUpsert) { s.SetFilters(v) }) diff --git a/backend/ent/usagecleanuptask_update.go b/backend/ent/usagecleanuptask_update.go index 604202c679..d233258bbe 100644 --- a/backend/ent/usagecleanuptask_update.go +++ b/backend/ent/usagecleanuptask_update.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "time" @@ -51,13 +51,13 @@ func (_u *UsageCleanupTaskUpdate) SetNillableStatus(v *string) *UsageCleanupTask } // SetFilters sets the "filters" field. -func (_u *UsageCleanupTaskUpdate) SetFilters(v json.RawMessage) *UsageCleanupTaskUpdate { +func (_u *UsageCleanupTaskUpdate) SetFilters(v jsontext.Value) *UsageCleanupTaskUpdate { _u.mutation.SetFilters(v) return _u } // AppendFilters appends value to the "filters" field. -func (_u *UsageCleanupTaskUpdate) AppendFilters(v json.RawMessage) *UsageCleanupTaskUpdate { +func (_u *UsageCleanupTaskUpdate) AppendFilters(v jsontext.Value) *UsageCleanupTaskUpdate { _u.mutation.AppendFilters(v) return _u } @@ -374,13 +374,13 @@ func (_u *UsageCleanupTaskUpdateOne) SetNillableStatus(v *string) *UsageCleanupT } // SetFilters sets the "filters" field. -func (_u *UsageCleanupTaskUpdateOne) SetFilters(v json.RawMessage) *UsageCleanupTaskUpdateOne { +func (_u *UsageCleanupTaskUpdateOne) SetFilters(v jsontext.Value) *UsageCleanupTaskUpdateOne { _u.mutation.SetFilters(v) return _u } // AppendFilters appends value to the "filters" field. -func (_u *UsageCleanupTaskUpdateOne) AppendFilters(v json.RawMessage) *UsageCleanupTaskUpdateOne { +func (_u *UsageCleanupTaskUpdateOne) AppendFilters(v jsontext.Value) *UsageCleanupTaskUpdateOne { _u.mutation.AppendFilters(v) return _u } diff --git a/backend/go.mod b/backend/go.mod index 2f07042ce5..b86368117f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module github.com/Wei-Shaw/sub2api -go 1.26.6 +go 1.27.0 require ( entgo.io/ent v0.14.5 diff --git a/backend/internal/handler/admin/grok_oauth_handler.go b/backend/internal/handler/admin/grok_oauth_handler.go index 696a24b419..fdab4473de 100644 --- a/backend/internal/handler/admin/grok_oauth_handler.go +++ b/backend/internal/handler/admin/grok_oauth_handler.go @@ -615,8 +615,10 @@ func (h *GrokOAuthHandler) ResetQuota(c *gin.Context) { response.BadRequest(c, "grok quota service is not enabled") return } + // ResetQuota 恒返回 GROK_QUOTA_RESET_UNSUPPORTED(xAI 无 OAuth 配额重置接口),err != nil 恒真为预期。 + //nolint:staticcheck // SA4023 result, err := h.quotaService.ResetQuota(c.Request.Context(), accountID) - if err != nil { + if err != nil { //nolint:staticcheck // SA4023 response.ErrorFrom(c, err) return } diff --git a/backend/internal/handler/admin/setting_handler_update.go b/backend/internal/handler/admin/setting_handler_update.go index 3775c01e11..b2c001fe73 100644 --- a/backend/internal/handler/admin/setting_handler_update.go +++ b/backend/internal/handler/admin/setting_handler_update.go @@ -441,7 +441,7 @@ func buildSettingKeyByJSONName() map[string]string { out := make(map[string]string, t.NumField()) for i := 0; i < t.NumField(); i++ { field := t.Field(i) - if field.Type.Kind() == reflect.Ptr { + if field.Type.Kind() == reflect.Pointer { continue } name, _, _ := strings.Cut(field.Tag.Get("json"), ",") diff --git a/backend/internal/handler/auth_oidc_oauth.go b/backend/internal/handler/auth_oidc_oauth.go index 0367e4cff0..13c26cf18c 100644 --- a/backend/internal/handler/auth_oidc_oauth.go +++ b/backend/internal/handler/auth_oidc_oauth.go @@ -1144,10 +1144,10 @@ func (k oidcJWK) publicKey() (any, error) { if err != nil { return nil, fmt.Errorf("decode ec y: %w", err) } - if !curve.IsOnCurve(x, y) { + if !curve.IsOnCurve(x, y) { //nolint:staticcheck // JWK 以裸坐标给出公钥;替换为 ecdsa.ParseUncompressedPublicKey 需改变点编码,待单独迁移 return nil, errors.New("ec point is not on curve") } - return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil + return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil //nolint:staticcheck // 同上 default: return nil, fmt.Errorf("unsupported jwk kty: %s", k.Kty) } diff --git a/backend/internal/repository/http_upstream_http2_keepalive_test.go b/backend/internal/repository/http_upstream_http2_keepalive_test.go index ead61da3b2..66a30dab0e 100644 --- a/backend/internal/repository/http_upstream_http2_keepalive_test.go +++ b/backend/internal/repository/http_upstream_http2_keepalive_test.go @@ -1,7 +1,10 @@ package repository import ( + "context" + "crypto/x509" "net/http" + "net/http/httptest" "net/url" "testing" "time" @@ -19,6 +22,16 @@ func http2KeepAliveTestPoolSettings() poolSettings { } } +// requireHTTP2Configured 断言 http2 已显式挂到 http.Transport 上。 +// x/net/http2 在 go1.27 && !http2legacy 下是标准库 HTTP/2 的包装:ConfigureTransports 通过 +// Transport.RegisterProtocol("http/2") 注册配置并打开 Protocols.HTTP2(TLSNextProto 不承载 h2 入口), +// ReadIdleTimeout/PingTimeout 在建连时映射为 http.HTTP2Config.SendPingTimeout/PingTimeout。 +func requireHTTP2Configured(t *testing.T, tr *http.Transport, msg string) { + t.Helper() + require.NotNil(t, tr.Protocols, msg) + require.True(t, tr.Protocols.HTTP2(), msg) +} + // Codex/OpenAI 上游改走 HTTP/2 后,池化连接被代理/NAT 静默掐断会成为“死连接”: // 两端都以为连接存活,请求落上去会挂到 TCP 重传超时(分钟级)才失败。Go 的 // http2.Transport 默认 ReadIdleTimeout=0(不发健康 PING),无法检测这种死连接。 @@ -34,7 +47,7 @@ func TestEnableOpenAIHTTP2KeepAlive_EnablesPingHealthCheck(t *testing.T) { require.Positive(t, h2.ReadIdleTimeout, "必须启用空闲 PING 探测以剔除死连接") require.Equal(t, openAIHTTP2ReadIdleTimeout, h2.ReadIdleTimeout) require.Equal(t, openAIHTTP2PingTimeout, h2.PingTimeout, "PING 无响应必须有超时判定") - require.NotNil(t, tr.TLSNextProto["h2"], "http2 必须已挂到底层 http.Transport 上") + requireHTTP2Configured(t, tr, "http2 必须已挂到底层 http.Transport 上") } // openai_h2 模式构建的 Transport 必须带上 H2 PING 健康探测,从源头剔除死连接。 @@ -42,17 +55,45 @@ func TestBuildUpstreamTransport_OpenAIH2_EnablesPingHealthCheck(t *testing.T) { tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeOpenAIH2) require.NoError(t, err) require.True(t, tr.ForceAttemptHTTP2, "openai_h2 必须启用 HTTP/2") - require.NotNil(t, tr.TLSNextProto["h2"], "openai_h2 必须显式配置 http2 以启用 ReadIdleTimeout") + requireHTTP2Configured(t, tr, "openai_h2 必须显式配置 http2 以启用 ReadIdleTimeout") } // 非 H2 模式(default/h1)不应因本次改动被误配置:default 走 Go 自动 H2(惰性配置, -// 构建时 TLSNextProto 仍为空),h1 模式显式禁用 H2。避免波及 Claude/Gemini 热路径。 +// 构建时 Protocols/TLSNextProto 仍为空),h1 模式显式禁用 H2。避免波及 Claude/Gemini 热路径。 func TestBuildUpstreamTransport_NonOpenAIH2_NotEagerlyConfigured(t *testing.T) { tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeDefault) require.NoError(t, err) + require.Nil(t, tr.Protocols, "default 模式不应在构建期主动配置 http2 keepalive") require.Nil(t, tr.TLSNextProto["h2"], "default 模式不应在构建期主动配置 http2 keepalive") } +// openai_h2 模式构建的 Transport 必须真正以 HTTP/2 与上游通信,PING 健康探测才有载体: +// 自定义 DialContext 下 Go 不会自动启用 H2,全靠 enableOpenAIHTTP2KeepAlive 的显式配置。 +func TestBuildUpstreamTransport_OpenAIH2_NegotiatesHTTP2(t *testing.T) { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + srv.EnableHTTP2 = true + srv.StartTLS() + defer srv.Close() + + tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeOpenAIH2) + require.NoError(t, err) + defer tr.CloseIdleConnections() + require.NotNil(t, tr.TLSClientConfig) + roots := x509.NewCertPool() + roots.AddCert(srv.Certificate()) + tr.TLSClientConfig.RootCAs = roots + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) + require.NoError(t, err) + resp, err := tr.RoundTrip(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, 2, resp.ProtoMajor, "openai_h2 必须协商到 HTTP/2") +} + // 死连接在经 HTTP 代理(CONNECT 隧道)时最高发,这是带 proxy 账号的真实生产路径: // 显式 http2 配置须与 Transport.Proxy 同时正确生效,不能相互干扰。 func TestBuildUpstreamTransport_OpenAIH2_WithHTTPProxy_EnablesKeepAlive(t *testing.T) { @@ -62,6 +103,6 @@ func TestBuildUpstreamTransport_OpenAIH2_WithHTTPProxy_EnablesKeepAlive(t *testi tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), proxyURL, upstreamProtocolModeOpenAIH2) require.NoError(t, err) require.True(t, tr.ForceAttemptHTTP2) - require.NotNil(t, tr.TLSNextProto["h2"], "经代理的 openai_h2 也必须启用 http2 keepalive") + requireHTTP2Configured(t, tr, "经代理的 openai_h2 也必须启用 http2 keepalive") require.NotNil(t, tr.Proxy, "HTTP 代理仍须通过 Transport.Proxy 生效") } diff --git a/backend/internal/repository/usage_log_repo_request_type_test.go b/backend/internal/repository/usage_log_repo_request_type_test.go index c69f374d41..a67c93c345 100644 --- a/backend/internal/repository/usage_log_repo_request_type_test.go +++ b/backend/internal/repository/usage_log_repo_request_type_test.go @@ -780,7 +780,7 @@ func (s usageLogScannerStub) Scan(dest ...any) error { } for i := range dest { dv := reflect.ValueOf(dest[i]) - if dv.Kind() != reflect.Ptr { + if dv.Kind() != reflect.Pointer { return fmt.Errorf("dest[%d] is not pointer", i) } dv.Elem().Set(reflect.ValueOf(s.values[i])) diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 30c7a1a240..ab312c4852 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -7,7 +7,7 @@ # ============================================================================= ARG NODE_IMAGE=node:24-alpine -ARG GOLANG_IMAGE=golang:1.26.6-alpine +ARG GOLANG_IMAGE=golang:1.27.0-alpine ARG ALPINE_IMAGE=alpine:3.20 ARG GOPROXY=https://goproxy.cn,direct ARG GOSUMDB=sum.golang.google.cn From 3b8177642905323201ddac5ac4e27b1f310ab9b9 Mon Sep 17 00:00:00 2001 From: feeeei Date: Sun, 23 Aug 2026 20:41:45 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(test):=20grok=20QueryQuota=20=E7=94=A8?= =?UTF-8?q?=E4=BE=8B=E6=8E=92=E9=99=A4=E5=90=8E=E5=8F=B0=20/v1/models=20?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E8=AF=B7=E6=B1=82=EF=BC=8C=E6=B6=88=E9=99=A4?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E8=AE=A1=E6=95=B0=E7=AB=9E=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QueryQuota 返回前经 scheduleGrokObservedModelsSync 异步拉取 GET /v1/models, 该请求是否早于 upstream.snapshot() 落到 mock 取决于调度时序,三个精确断言 请求数的用例约 0.5% 偶发多出一条。新增 quotaSnapshot 只返回配额探测链路的 请求,三处断言改用它。 --- .../service/grok_quota_service_test.go | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/grok_quota_service_test.go b/backend/internal/service/grok_quota_service_test.go index 70ee2c8d72..9d94d1b989 100644 --- a/backend/internal/service/grok_quota_service_test.go +++ b/backend/internal/service/grok_quota_service_test.go @@ -301,6 +301,22 @@ func (u *grokHybridUpstream) snapshot() ([]*http.Request, [][]byte) { return requests, bodies } +// quotaSnapshot 返回配额探测链路的请求及请求体,不含 scheduleGrokObservedModelsSync +// 在 QueryQuota 返回后异步发出的 GET /v1/models:该请求是否已落到上游取决于调度时序。 +func (u *grokHybridUpstream) quotaSnapshot() ([]*http.Request, [][]byte) { + requests, bodies := u.snapshot() + quotaRequests := make([]*http.Request, 0, len(requests)) + quotaBodies := make([][]byte, 0, len(bodies)) + for i, req := range requests { + if req.URL.Path == "/v1/models" { + continue + } + quotaRequests = append(quotaRequests, req) + quotaBodies = append(quotaBodies, bodies[i]) + } + return quotaRequests, quotaBodies +} + func (r *grokQuotaProxyRepo) GetByID(_ context.Context, id int64) (*Proxy, error) { r.calls++ return r.proxies[id], nil @@ -741,7 +757,7 @@ func TestGrokQuotaServiceQueryQuotaFreeFallsBackToGrok45(t *testing.T) { require.EqualValues(t, 2_000_000, *result.Snapshot.Tokens.Limit) require.True(t, result.HeadersObserved) - requests, bodies := upstream.snapshot() + requests, bodies := upstream.quotaSnapshot() require.Len(t, requests, 3) responseCalls := 0 for i, req := range requests { @@ -781,7 +797,7 @@ func TestGrokQuotaServiceQueryQuotaPaidBillingSkipsActiveProbe(t *testing.T) { require.Empty(t, result.Model) require.Nil(t, result.LocalUsage24h) - requests, _ := upstream.snapshot() + requests, _ := upstream.quotaSnapshot() require.Len(t, requests, 2) for _, req := range requests { require.Equal(t, "/v1/billing", req.URL.Path) @@ -806,7 +822,7 @@ func TestGrokQuotaServiceQueryQuotaCustomPaidMonthlyLimitSkipsActiveProbe(t *tes require.InDelta(t, monthlyLimit, *result.Billing.MonthlyLimitCents, 1e-9) require.Nil(t, result.Snapshot) - requests, _ := upstream.snapshot() + requests, _ := upstream.quotaSnapshot() require.Len(t, requests, 2) for _, req := range requests { require.Equal(t, "/v1/billing", req.URL.Path) From 73aabc861cacbfc9240bb306ef0aa04511d0164f Mon Sep 17 00:00:00 2001 From: feeeei Date: Mon, 24 Aug 2026 12:19:50 +0800 Subject: [PATCH 3/3] =?UTF-8?q?build:=20=E5=8F=96=E6=B6=88=20gosec=20G703/?= =?UTF-8?q?G704=20=E5=85=A8=E5=B1=80=E6=8E=92=E9=99=A4=EF=BC=8C=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E4=BB=A3=E7=A0=81=E9=80=90=E7=82=B9=20nolint=E3=80=81?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=E6=8C=89=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E8=B1=81=E5=85=8D=EF=BC=9BDEV=5FGUIDE=20=E5=90=8C=E6=AD=A5=20g?= =?UTF-8?q?olangci-lint=20v2.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEV_GUIDE.md | 6 +++--- backend/.golangci.yml | 14 ++++++++++---- backend/internal/pkg/servertiming/http.go | 4 ++-- backend/internal/service/gateway_service.go | 6 +++--- backend/internal/service/pricing_service.go | 2 +- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index cb1ffabdb3..2f440d0f62 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -34,8 +34,8 @@ ### 开发工具 ```bash -# golangci-lint(CI 用 v2.9,本地建议装同一版以免版本差异带来的噪音) -go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9 +# golangci-lint(CI 用 v2.13,本地建议装同一版以免版本差异带来的噪音) +go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13 # pnpm (前端包管理) npm install -g pnpm @@ -47,7 +47,7 @@ npm install -g pnpm | Workflow | 触发条件 | 检查内容 | |----------|----------|----------| -| **backend-ci.yml** | push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.9 | +| **backend-ci.yml** | push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.13 | | **security-scan.yml** | push, pull_request, 每周一 | govulncheck + gosec + pnpm audit | | **release.yml** | tag `v*` | 构建发布(PR 不触发) | diff --git a/backend/.golangci.yml b/backend/.golangci.yml index a4aed11470..08d1b61f45 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -57,10 +57,6 @@ linters: - G304 - G306 - G404 - # G703/G704 是 G304/G107 的污点分析版:网关本职就是按配置转发到上游 URL、按配置写本地文件, - # 污点来源即配置本身,规则不适用。 - - G703 - - G704 severity: high confidence: high errcheck: @@ -125,6 +121,16 @@ linters: # Default: true — must be true, ent generates 130K+ lines of code generated-is-used: true + exclusions: + rules: + # G703/G704 污点分析在测试文件中只会命中 httptest mock 与测试者自设的 + # 环境变量路径,不构成攻击面;且该分析跨环境结果不稳定(本地/CI 报告的 + # 位置集合不同),逐点 nolint 无法收敛,故按路径豁免。生产代码不豁免, + # 必须逐点 //nolint:gosec 并写明信任边界。 + - path: '_test\.go$' + linters: [ gosec ] + text: 'G70[34]' + formatters: enable: - gofmt diff --git a/backend/internal/pkg/servertiming/http.go b/backend/internal/pkg/servertiming/http.go index e326e24302..2ad8a04ae1 100644 --- a/backend/internal/pkg/servertiming/http.go +++ b/backend/internal/pkg/servertiming/http.go @@ -53,10 +53,10 @@ func Do(client *http.Client, req *http.Request) (*http.Response, error) { client = http.DefaultClient } if req == nil || !Active(req.Context()) { - return client.Do(req) + return client.Do(req) //nolint:gosec // G704: 通用埋点包装器,不构造 URL;请求由调用方构造,SSRF 信任边界在调用方 } startedAt := time.Now() - response, err := client.Do(req) + response, err := client.Do(req) //nolint:gosec // G704: 同上 RecordDependency(req.Context(), dependencyModule(req), startedAt, time.Now()) return response, err } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 0c0140b338..37bab2f97d 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -1520,19 +1520,19 @@ func (s *GatewayService) initDebugGatewayBodyFile(path string) { } // 如果 path 指向一个已存在的目录,自动追加默认文件名 - if info, err := os.Stat(path); err == nil && info.IsDir() { + if info, err := os.Stat(path); err == nil && info.IsDir() { //nolint:gosec // G703: path 仅来自启动环境变量 SUB2API_DEBUG_GATEWAY_BODY(运维配置),非请求输入 path = filepath.Join(path, debugGatewayBodyDefaultFilename) } // 确保父目录存在 if dir := filepath.Dir(path); dir != "." { - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0755); err != nil { //nolint:gosec // G703: 同上 slog.Error("failed to create gateway debug log directory", "dir", dir, "error", err) return } } - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) //nolint:gosec // G703: 同上 if err != nil { slog.Error("failed to open gateway debug log file", "path", path, "error", err) return diff --git a/backend/internal/service/pricing_service.go b/backend/internal/service/pricing_service.go index f7733ae001..6ae24dd39e 100644 --- a/backend/internal/service/pricing_service.go +++ b/backend/internal/service/pricing_service.go @@ -596,7 +596,7 @@ func (s *PricingService) useFallbackPricing() error { } pricingFile := s.getPricingFilePath() - if err := os.WriteFile(pricingFile, data, 0644); err != nil { + if err := os.WriteFile(pricingFile, data, 0644); err != nil { //nolint:gosec // G703: 路径为配置的数据目录 + 硬编码文件名,非请求输入 logger.LegacyPrintf("service.pricing", "[Pricing] Failed to copy fallback: %v", err) }