diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..d13785c3 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,52 @@ +version: "2" + +run: + timeout: 5m + tests: false + +linters: + default: none + enable: + # 基础检查 + - govet + - staticcheck + - errcheck + - ineffassign + - unused + + # 代码坏味道 + - dupl # 重复代码 + - mnd # 魔法数字 + - gocyclo # 圈复杂度 + - cyclop # 包/函数复杂度 + - nestif # if 嵌套太深 + - maintidx # 维护性指数 + - revive # 风格/命名/坏味道 + - gocritic # 各类代码问题 + + settings: + dupl: + threshold: 150 + + gocyclo: + min-complexity: 20 + + cyclop: + max-complexity: 15 + package-average: 10 + + funlen: + lines: 150 + statements: 80 + + nestif: + min-complexity: 5 + + mnd: + checks: + - argument + - case + - condition + - operation + - return + - assign diff --git a/Agents.md b/Agents.md index 602adc4f..afe35f5d 100644 --- a/Agents.md +++ b/Agents.md @@ -473,21 +473,24 @@ export function FeatureMain() { ## 五、代码质量与审查规范 -### 5.1 统一代码检查与提交 +### 5.1 Make 指令 + +| 指令 | 触发时机 | 说明 | +|------|----------|------| +| `make code-check` | **提交前必须执行** | 前端 TypeScript 类型检查 + ESLint 静态分析;后端 `golangci-lint` 代码规范扫描 | +| `make build-test` | 功能完成后验证 | 前后端**并行**完整编译测试(`pnpm build` + `go build`),快速发现编译错误 | +| `make swagger` | 新增/修改 API 后 | 自动生成/更新 Swagger 文档(`docs/swagger.json`) | +| `make build-embedded` | 发布前 | 前端静态导出嵌入后端,生成单二进制产物 `bin/wavelet` | +| `make license` | 新增 Go 文件后 | 自动为所有 Go 源文件添加/更新 License Header | +| `make license-check` | CI 流水线 | 校验所有 Go 文件的 License Header 是否合规 | > [!IMPORTANT] -> **代码开发完成后,提交前必须在项目根目录运行 `make code-check` 进行本地代码质量与风格检查,确保前端 ESLint 没有报错和警告(`--max-warnings 0`)。** +> **代码开发完成后,提交前必须运行 `make code-check`,所有检查全部通过后方可提交。** -```bash -# 执行根目录的检查指令 -make code-check -``` +--- ### 5.2 后端规范检查 -**基础检查**: -- 后端 Go 代码需要通过 CodeQL 扫描。较复杂的逻辑建议结合 Copilot 检查。 - **API 文档**: - 所有 HTTP 接口都必须编写完整的 Swagger 注释。提交前需运行 `make swagger` 自动生成与更新接口文档。 diff --git a/Makefile b/Makefile index 48ffbda2..3adad7b5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: swagger license license-check build-embedded +.PHONY: swagger license license-check build-embedded build-test swagger: scripts/swagger.sh @@ -16,4 +16,21 @@ build-embedded: go build -tags embed_frontend -o bin/wavelet main.go code-check: - cd frontend && npx eslint . --max-warnings 0 + cd frontend && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0 + golangci-lint run + +build-test: + @echo "==> Running frontend and backend build tests in parallel..." + @PIDS=""; \ + STATUS=0; \ + ( cd frontend && pnpm build 2>&1 | sed 's/^/[frontend] /' ) & PIDS="$$PIDS $$!"; \ + ( go build -o /dev/null ./... 2>&1 | sed 's/^/[backend] /' ) & PIDS="$$PIDS $$!"; \ + for PID in $$PIDS; do \ + wait $$PID || STATUS=1; \ + done; \ + if [ $$STATUS -eq 0 ]; then \ + echo "==> All build tests passed."; \ + else \ + echo "==> Build test FAILED." >&2; \ + exit 1; \ + fi diff --git a/frontend/components/common/settings/profile.tsx b/frontend/components/common/settings/profile.tsx index 627d4b5d..ba417382 100644 --- a/frontend/components/common/settings/profile.tsx +++ b/frontend/components/common/settings/profile.tsx @@ -45,7 +45,7 @@ import { } from "@/components/ui/dialog" import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select" import {Label} from "@/components/ui/label" -import {AuthService} from "@/lib/services" +import {AuthService, ChangePasswordRequest, UpdateProfileRequest} from "@/lib/services" import {UploadService} from "@/lib/services/upload/upload.service" import {ImageCrop, ImageCropApply, ImageCropContent, ImageCropReset} from "@/components/ui/image-crop" import {toast} from "sonner" @@ -92,7 +92,7 @@ export function ProfileMain() { }, [user, isEditDialogOpen]) const changePasswordMutation = useMutation({ - mutationFn: AuthService.changePassword, + mutationFn: (req: ChangePasswordRequest) => AuthService.changePassword(req), onSuccess: () => { toast.success("密码修改成功") setOldPassword("") @@ -122,7 +122,7 @@ export function ProfileMain() { } const updateProfileMutation = useMutation({ - mutationFn: AuthService.updateProfile, + mutationFn: (req: UpdateProfileRequest) => AuthService.updateProfile(req), onSuccess: () => { toast.success("个人信息修改成功") setIsEditDialogOpen(false) diff --git a/internal/apps/admin/auth_source/routers_test.go b/internal/apps/admin/auth_source/routers_test.go index 043dbbfc..00e59006 100644 --- a/internal/apps/admin/auth_source/routers_test.go +++ b/internal/apps/admin/auth_source/routers_test.go @@ -81,11 +81,11 @@ func TestListAuthSources(t *testing.T) { } var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) dataBytes, _ := json.Marshal(resp.Data) var sources []model.AuthSource - json.Unmarshal(dataBytes, &sources) + _ = json.Unmarshal(dataBytes, &sources) if len(sources) != 1 { t.Errorf("expected 1 auth source, got %d", len(sources)) diff --git a/internal/apps/admin/logs/routers.go b/internal/apps/admin/logs/routers.go index 7fd4a731..3e3f3fdb 100644 --- a/internal/apps/admin/logs/routers.go +++ b/internal/apps/admin/logs/routers.go @@ -103,7 +103,7 @@ func HandleLogWebSocket(c *gin.Context) { if err != nil { return } - defer conn.Close() + defer func() { _ = conn.Close() }() // 订阅 ring buffer ch := logger.GlobalRingBuffer.Subscribe() @@ -296,7 +296,7 @@ func GetAccessLogs(c *gin.Context) { c.JSON(http.StatusInternalServerError, util.Err("查询 ClickHouse 日志明细失败: "+err.Error())) return } - defer rows.Close() + defer func() { _ = rows.Close() }() var list []accessLogItem var fetchUserIDs []uint64 @@ -424,7 +424,7 @@ func GetLogsAnalytics(c *gin.Context) { } if err == nil { - defer trendRows.Close() + defer func() { _ = trendRows.Close() }() for trendRows.Next() { var dt time.Time var cnt uint64 @@ -454,7 +454,7 @@ func GetLogsAnalytics(c *gin.Context) { browserCounts := make(map[string]uint64) if err == nil { - defer uaRows.Close() + defer func() { _ = uaRows.Close() }() for uaRows.Next() { var ua string var cnt uint64 @@ -493,7 +493,7 @@ func GetLogsAnalytics(c *gin.Context) { userCountMap := make(map[uint64]uint64) if err == nil { - defer userRows.Close() + defer func() { _ = userRows.Close() }() for userRows.Next() { var uid uint64 var cnt uint64 diff --git a/internal/apps/admin/system_config/routers.go b/internal/apps/admin/system_config/routers.go index f51d9fe1..f214495f 100644 --- a/internal/apps/admin/system_config/routers.go +++ b/internal/apps/admin/system_config/routers.go @@ -206,7 +206,7 @@ func UpdateSystemConfig(c *gin.Context) { updates := map[string]interface{}{ "description": req.Description, } - if !(key == model.ConfigKeySMTPPassword && req.Value == "******") { + if key != model.ConfigKeySMTPPassword || req.Value != "******" { updates["value"] = req.Value config.Value = req.Value } diff --git a/internal/apps/admin/system_config/routers_test.go b/internal/apps/admin/system_config/routers_test.go index 67132747..8b60d57a 100644 --- a/internal/apps/admin/system_config/routers_test.go +++ b/internal/apps/admin/system_config/routers_test.go @@ -140,11 +140,11 @@ func TestListSystemConfigs(t *testing.T) { } var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) dataBytes, _ := json.Marshal(resp.Data) var configs []model.SystemConfig - json.Unmarshal(dataBytes, &configs) + _ = json.Unmarshal(dataBytes, &configs) // Defaults seed 23 configurations if len(configs) != 23 { @@ -158,11 +158,11 @@ func TestListSystemConfigs(t *testing.T) { router.ServeHTTP(w, req) var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) dataBytes, _ := json.Marshal(resp.Data) var configs []model.SystemConfig - json.Unmarshal(dataBytes, &configs) + _ = json.Unmarshal(dataBytes, &configs) if len(configs) != 1 || configs[0].Key != model.ConfigKeyMaxAPIKeysPerUser { t.Errorf("expected 1 business config (max_api_keys_per_user), got %d: %v", len(configs), configs) @@ -187,11 +187,11 @@ func TestGetSystemConfig(t *testing.T) { } var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) dataBytes, _ := json.Marshal(resp.Data) var cfg model.SystemConfig - json.Unmarshal(dataBytes, &cfg) + _ = json.Unmarshal(dataBytes, &cfg) if cfg.Value != "Wavelet" { t.Errorf("expected 'Wavelet', got '%s'", cfg.Value) @@ -240,7 +240,7 @@ func TestUpdateSystemConfig(t *testing.T) { // Verify Redis var redisConfig model.SystemConfig - db.HGetJSON(context.Background(), model.SystemConfigRedisHashKey, model.ConfigKeySiteName, &redisConfig) + _ = db.HGetJSON(context.Background(), model.SystemConfigRedisHashKey, model.ConfigKeySiteName, &redisConfig) if redisConfig.Value != "Super Site Name" { t.Errorf("redis cache value not updated, got '%s'", redisConfig.Value) } @@ -275,7 +275,7 @@ func TestTestSMTP(t *testing.T) { if err != nil { t.Fatalf("failed to start mock smtp server: %v", err) } - defer l.Close() + defer func() { _ = l.Close() }() port := l.Addr().(*net.TCPAddr).Port @@ -284,40 +284,40 @@ func TestTestSMTP(t *testing.T) { if err != nil { return } - defer conn.Close() + defer func() { _ = conn.Close() }() writer := bufio.NewWriter(conn) reader := bufio.NewReader(conn) tp := textproto.NewReader(reader) // 220 Ready - writer.WriteString("220 mock.smtp.com SMTP Ready\r\n") - writer.Flush() + _, _ = writer.WriteString("220 mock.smtp.com SMTP Ready\r\n") + _ = writer.Flush() // Read HELO/EHLO - tp.ReadLine() - writer.WriteString("250-mock.smtp.com\r\n250 AUTH PLAIN\r\n") - writer.Flush() + _, _ = tp.ReadLine() + _, _ = writer.WriteString("250-mock.smtp.com\r\n250 AUTH PLAIN\r\n") + _ = writer.Flush() // Read AUTH PLAIN - tp.ReadLine() - writer.WriteString("235 Authentication successful\r\n") - writer.Flush() + _, _ = tp.ReadLine() + _, _ = writer.WriteString("235 Authentication successful\r\n") + _ = writer.Flush() // Read MAIL FROM - tp.ReadLine() - writer.WriteString("250 OK\r\n") - writer.Flush() + _, _ = tp.ReadLine() + _, _ = writer.WriteString("250 OK\r\n") + _ = writer.Flush() // Read RCPT TO - tp.ReadLine() - writer.WriteString("250 OK\r\n") - writer.Flush() + _, _ = tp.ReadLine() + _, _ = writer.WriteString("250 OK\r\n") + _ = writer.Flush() // Read DATA - tp.ReadLine() - writer.WriteString("354 Start mail input\r\n") - writer.Flush() + _, _ = tp.ReadLine() + _, _ = writer.WriteString("354 Start mail input\r\n") + _ = writer.Flush() // Read body lines until dot for { @@ -326,13 +326,13 @@ func TestTestSMTP(t *testing.T) { break } } - writer.WriteString("250 OK\r\n") - writer.Flush() + _, _ = writer.WriteString("250 OK\r\n") + _ = writer.Flush() // Read QUIT - tp.ReadLine() - writer.WriteString("221 Bye\r\n") - writer.Flush() + _, _ = tp.ReadLine() + _, _ = writer.WriteString("221 Bye\r\n") + _ = writer.Flush() }() payload := TestSMTPRequest{ diff --git a/internal/apps/admin/task/routers_test.go b/internal/apps/admin/task/routers_test.go index 55a0c88e..5b9b8edb 100644 --- a/internal/apps/admin/task/routers_test.go +++ b/internal/apps/admin/task/routers_test.go @@ -45,7 +45,7 @@ func setupTaskTestEnvironment(t *testing.T) func() { }) return func() { if task.AsynqClient != nil { - task.AsynqClient.Close() + _ = task.AsynqClient.Close() task.AsynqClient = nil } cleanup() @@ -89,11 +89,11 @@ func TestListTaskTypes(t *testing.T) { } var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) dataBytes, _ := json.Marshal(resp.Data) var taskMetas []task.TaskMeta - json.Unmarshal(dataBytes, &taskMetas) + _ = json.Unmarshal(dataBytes, &taskMetas) if len(taskMetas) == 0 { t.Error("expected at least one dispatchable task type") @@ -131,7 +131,7 @@ func TestDispatchTask(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code, "Body: %s", w.Body.String()) var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Empty(t, resp.ErrorMsg) assert.NotNil(t, resp.Data) diff --git a/internal/apps/admin/template/routers_test.go b/internal/apps/admin/template/routers_test.go index d418ea69..a983fc30 100644 --- a/internal/apps/admin/template/routers_test.go +++ b/internal/apps/admin/template/routers_test.go @@ -136,11 +136,11 @@ func TestListTemplates(t *testing.T) { } var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) dataBytes, _ := json.Marshal(resp.Data) var templates []model.Template - json.Unmarshal(dataBytes, &templates) + _ = json.Unmarshal(dataBytes, &templates) if len(templates) != 2 { t.Errorf("expected 2 templates, got %d", len(templates)) diff --git a/internal/apps/admin/user/routers_test.go b/internal/apps/admin/user/routers_test.go index 1eb9ccc4..e0cff3ea 100644 --- a/internal/apps/admin/user/routers_test.go +++ b/internal/apps/admin/user/routers_test.go @@ -131,11 +131,11 @@ func TestListUsers(t *testing.T) { router.ServeHTTP(w, req) var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) dataBytes, _ := json.Marshal(resp.Data) var listResp listUsersResponse - json.Unmarshal(dataBytes, &listResp) + _ = json.Unmarshal(dataBytes, &listResp) if len(listResp.Users) != 1 || listResp.Users[0].ID != 1001 { t.Errorf("expected 1 user with ID 1001, got total %d", len(listResp.Users)) @@ -148,11 +148,11 @@ func TestListUsers(t *testing.T) { router.ServeHTTP(w, req) var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) dataBytes, _ := json.Marshal(resp.Data) var listResp listUsersResponse - json.Unmarshal(dataBytes, &listResp) + _ = json.Unmarshal(dataBytes, &listResp) if len(listResp.Users) != 1 || listResp.Users[0].Username != "bob" { t.Errorf("expected bob, got %v", listResp.Users) @@ -226,7 +226,7 @@ func TestUpdateUserStatus(t *testing.T) { } var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp.ErrorMsg != cannotDisable { t.Errorf("expected error message '%s', got '%s'", cannotDisable, resp.ErrorMsg) } @@ -326,7 +326,7 @@ func TestCreateUser(t *testing.T) { } var resp util.ResponseAny - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp.ErrorMsg != usernameExists { t.Errorf("expected error '%s', got '%s'", usernameExists, resp.ErrorMsg) } diff --git a/internal/apps/oauth/oauth_test.go b/internal/apps/oauth/oauth_test.go index 1b58b2f1..142df680 100644 --- a/internal/apps/oauth/oauth_test.go +++ b/internal/apps/oauth/oauth_test.go @@ -214,15 +214,6 @@ func oidcDiscoveryResponse() *http.Response { } } -func jwksResponse() *http.Response { - jwksJSON, _ := json.Marshal(testJWKS) - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(jwksJSON)), - Header: make(http.Header), - } -} - type mockClaims struct { ID uint64 `json:"id"` Issuer string `json:"iss"` @@ -822,7 +813,7 @@ func TestCallbackBind(t *testing.T) { router.GET("/test-helper/login-777", func(c *gin.Context) { session := sessions.Default(c) session.Set(UserIDKey, uint64(777)) - session.Save() + _ = session.Save() c.String(200, "ok") }) @@ -874,7 +865,7 @@ func TestCallbackBind(t *testing.T) { router.GET("/test-helper/login-888", func(c *gin.Context) { session := sessions.Default(c) session.Set(UserIDKey, uint64(888)) - session.Save() + _ = session.Save() c.String(200, "ok") }) @@ -937,7 +928,7 @@ func TestExternalAccountsListAndDelete(t *testing.T) { router.GET("/test-helper/login-555", func(c *gin.Context) { session := sessions.Default(c) session.Set(UserIDKey, uint64(555)) - session.Save() + _ = session.Save() c.String(200, "ok") }) diff --git a/internal/apps/upload/file_server.go b/internal/apps/upload/file_server.go index 3b6007b9..b3d16a33 100644 --- a/internal/apps/upload/file_server.go +++ b/internal/apps/upload/file_server.go @@ -82,7 +82,7 @@ func ServeFileByID(c *gin.Context) { } // Stream from CDN/S3 - defer obj.Body.Close() + defer func() { _ = obj.Body.Close() }() // Respond with the file content c.DataFromReader(http.StatusOK, obj.ContentLength, obj.ContentType, obj.Body, nil) diff --git a/internal/apps/upload/routers.go b/internal/apps/upload/routers.go index ec8a120a..3773fb78 100644 --- a/internal/apps/upload/routers.go +++ b/internal/apps/upload/routers.go @@ -88,7 +88,7 @@ func UploadFile(c *gin.Context) { c.JSON(http.StatusOK, util.Err(ErrOpenFileFailed)) return } - defer file.Close() + defer func() { _ = file.Close() }() // 校验大小 if header.Size > maxUploadSize { @@ -318,7 +318,7 @@ func DownloadFile(c *gin.Context) { return } - defer obj.Body.Close() + defer func() { _ = obj.Body.Close() }() _, _ = io.Copy(c.Writer, obj.Body) } @@ -372,7 +372,7 @@ func BatchDownloadFiles(c *gin.Context) { // 开启实时 ZIP 压缩器并直接输出给 Response Writer zipWriter := zip.NewWriter(c.Writer) - defer zipWriter.Close() + defer func() { _ = zipWriter.Close() }() // 用于解决 ZIP 内部文件名称发生碰撞冲突的问题 usedNames := make(map[string]int) diff --git a/internal/apps/upload/routers_test.go b/internal/apps/upload/routers_test.go index ada65892..a3b34952 100644 --- a/internal/apps/upload/routers_test.go +++ b/internal/apps/upload/routers_test.go @@ -95,7 +95,7 @@ func createMultipartRequest(t *testing.T, fieldName, fileName string, fileConten func TestUploadFile(t *testing.T) { dbConn, _, cleanup := testhelper.SetupTestEnvironment(t) defer cleanup() - defer os.RemoveAll("uploads") // Clean up local files created during tests + defer func() { _ = os.RemoveAll("uploads") }() // Clean up local files created during tests authUser := &model.User{ID: 1001, Username: "test_user"} router := setupTestRouter(authUser) @@ -211,7 +211,7 @@ func TestUploadFile(t *testing.T) { } var resp testResponse - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp.ErrorMsg == "" || !strings.Contains(resp.ErrorMsg, ErrUnsupportedFormat) { t.Errorf("expected unsupported format error, got: %v", resp) } @@ -247,7 +247,7 @@ func TestUploadFile(t *testing.T) { } var resp2 testResponse - json.Unmarshal(w2.Body.Bytes(), &resp2) + _ = json.Unmarshal(w2.Body.Bytes(), &resp2) if resp2.ErrorMsg != "" { t.Fatalf("second upload was unsuccessful: %s", resp2.ErrorMsg) @@ -304,7 +304,7 @@ func TestUploadFile(t *testing.T) { } var resp testResponse - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp.ErrorMsg != "" { t.Fatalf("local upload failed: %s", resp.ErrorMsg) @@ -334,7 +334,7 @@ func TestUploadFile(t *testing.T) { func TestDownloadFile(t *testing.T) { dbConn, _, cleanup := testhelper.SetupTestEnvironment(t) defer cleanup() - defer os.RemoveAll("uploads") + defer func() { _ = os.RemoveAll("uploads") }() authUser := &model.User{ID: 1001, Username: "test_user"} router := setupTestRouter(authUser) @@ -403,7 +403,7 @@ func TestDownloadFile(t *testing.T) { func TestBatchDownloadFiles(t *testing.T) { dbConn, _, cleanup := testhelper.SetupTestEnvironment(t) defer cleanup() - defer os.RemoveAll("uploads") + defer func() { _ = os.RemoveAll("uploads") }() authUser := &model.User{ID: 1001, Username: "test_user"} router := setupTestRouter(authUser) @@ -495,7 +495,7 @@ func TestBatchDownloadFiles(t *testing.T) { t.Fatalf("failed to open zip file entry %s: %v", f.Name, err) } content, _ := io.ReadAll(rc) - rc.Close() + _ = rc.Close() extracted[f.Name] = string(content) } diff --git a/internal/apps/upload/utils.go b/internal/apps/upload/utils.go index 578e1a60..4e74106b 100644 --- a/internal/apps/upload/utils.go +++ b/internal/apps/upload/utils.go @@ -18,6 +18,7 @@ limitations under the License. package upload import ( + "errors" "fmt" "strings" ) @@ -27,7 +28,7 @@ const maxS3KeyLength = 1024 // ValidateS3Key validates an S3 object key for safety. func ValidateS3Key(key string) error { if key == "" { - return fmt.Errorf(ErrS3KeyRequired) + return errors.New(ErrS3KeyRequired) } if len(key) > maxS3KeyLength { @@ -35,11 +36,11 @@ func ValidateS3Key(key string) error { } if strings.HasPrefix(key, "/") { - return fmt.Errorf(ErrS3KeyStartsWithSlash) + return errors.New(ErrS3KeyStartsWithSlash) } if strings.Contains(key, "\x00") { - return fmt.Errorf(ErrS3KeyContainsNullBytes) + return errors.New(ErrS3KeyContainsNullBytes) } return nil diff --git a/internal/apps/user/controllers.go b/internal/apps/user/controllers.go index 0d3287ec..31a301da 100644 --- a/internal/apps/user/controllers.go +++ b/internal/apps/user/controllers.go @@ -125,7 +125,7 @@ func sendEmailVerificationCode(ctx context.Context, email, scene, templateName s // 存验证码,5分钟有效 if err := db.SetJSON(ctx, codeKey, code, 5*time.Minute); err != nil { - return fmt.Errorf(errGenerateEmailCodeFailed) + return errors.New(errGenerateEmailCodeFailed) } // 存冷却,60秒有效 _ = db.SetJSON(ctx, cooldownKey, "1", 60*time.Second) @@ -139,7 +139,7 @@ func sendEmailVerificationCode(ctx context.Context, email, scene, templateName s payloadBytes, _ := json.Marshal(payload) _, err = task.DispatchTask(ctx, task.TaskTypeSendEmail, payloadBytes, "system") if err != nil { - return fmt.Errorf(errDispatchEmailTaskFailed) + return errors.New(errDispatchEmailTaskFailed) } return nil } diff --git a/internal/logger/ringbuffer_test.go b/internal/logger/ringbuffer_test.go index a5762c78..67b7c43c 100644 --- a/internal/logger/ringbuffer_test.go +++ b/internal/logger/ringbuffer_test.go @@ -27,7 +27,7 @@ func TestLogRingBuffer_WriteAndQuery(t *testing.T) { rb := NewLogRingBuffer(5) // Write some logs - rb.Write([]byte("line1\nline2\nline3\n")) + _, _ = rb.Write([]byte("line1\nline2\nline3\n")) entries, hasMore := rb.Query(0, 10) assert.False(t, hasMore) @@ -43,7 +43,7 @@ func TestLogRingBuffer_WriteAndQuery(t *testing.T) { func TestLogRingBuffer_CapacityOverflow(t *testing.T) { rb := NewLogRingBuffer(3) - rb.Write([]byte("a\nb\nc\nd\ne\n")) + _, _ = rb.Write([]byte("a\nb\nc\nd\ne\n")) entries, hasMore := rb.Query(0, 10) assert.False(t, hasMore) @@ -56,7 +56,7 @@ func TestLogRingBuffer_CapacityOverflow(t *testing.T) { func TestLogRingBuffer_QueryLatest(t *testing.T) { rb := NewLogRingBuffer(10) - rb.Write([]byte("a\nb\nc\nd\ne\n")) + _, _ = rb.Write([]byte("a\nb\nc\nd\ne\n")) // Query latest 2 entries, hasMore := rb.Query(0, 2) @@ -69,7 +69,7 @@ func TestLogRingBuffer_QueryLatest(t *testing.T) { func TestLogRingBuffer_QueryByCursor(t *testing.T) { rb := NewLogRingBuffer(10) - rb.Write([]byte("a\nb\nc\nd\ne\n")) + _, _ = rb.Write([]byte("a\nb\nc\nd\ne\n")) // First get all to find indices all, _ := rb.Query(0, 10) @@ -87,7 +87,7 @@ func TestLogRingBuffer_QueryByCursor(t *testing.T) { func TestLogRingBuffer_QueryByCursorWithLimit(t *testing.T) { rb := NewLogRingBuffer(10) - rb.Write([]byte("a\nb\nc\nd\ne\n")) + _, _ = rb.Write([]byte("a\nb\nc\nd\ne\n")) // Query 2 entries before index 4 entries, hasMore := rb.Query(4, 2) @@ -107,7 +107,7 @@ func TestLogRingBuffer_QueryEmpty(t *testing.T) { func TestLogRingBuffer_QueryNonExistentCursor(t *testing.T) { rb := NewLogRingBuffer(5) - rb.Write([]byte("a\nb\n")) + _, _ = rb.Write([]byte("a\nb\n")) entries, hasMore := rb.Query(999, 10) assert.False(t, hasMore) @@ -122,7 +122,7 @@ func TestLogRingBuffer_Subscribe(t *testing.T) { ch := rb.Subscribe() defer rb.Unsubscribe(ch) - rb.Write([]byte("hello\n")) + _, _ = rb.Write([]byte("hello\n")) entry := <-ch assert.Equal(t, "hello", entry.Data) @@ -137,7 +137,7 @@ func TestLogRingBuffer_SubscribeMultiple(t *testing.T) { ch2 := rb.Subscribe() defer rb.Unsubscribe(ch2) - rb.Write([]byte("msg\n")) + _, _ = rb.Write([]byte("msg\n")) e1 := <-ch1 e2 := <-ch2 @@ -148,7 +148,7 @@ func TestLogRingBuffer_SubscribeMultiple(t *testing.T) { func TestLogRingBuffer_WriteNoNewline(t *testing.T) { rb := NewLogRingBuffer(5) - rb.Write([]byte("partial")) + _, _ = rb.Write([]byte("partial")) entries, _ := rb.Query(0, 10) assert.Equal(t, 1, len(entries)) @@ -169,7 +169,7 @@ func TestLogRingBuffer_WriteEmpty(t *testing.T) { func TestLogRingBuffer_QueryAfterOverflow(t *testing.T) { rb := NewLogRingBuffer(3) - rb.Write([]byte("1\n2\n3\n4\n5\n6\n7\n")) + _, _ = rb.Write([]byte("1\n2\n3\n4\n5\n6\n7\n")) entries, hasMore := rb.Query(0, 10) assert.False(t, hasMore) @@ -187,7 +187,7 @@ func TestLogRingBuffer_QueryAfterOverflow(t *testing.T) { func TestLogRingBuffer_NextCursor(t *testing.T) { rb := NewLogRingBuffer(10) - rb.Write([]byte("a\nb\nc\nd\ne\n")) + _, _ = rb.Write([]byte("a\nb\nc\nd\ne\n")) // Query latest 2, should return next_cursor pointing to first returned entry entries, _ := rb.Query(0, 2) diff --git a/internal/model/task_execution_test.go b/internal/model/task_execution_test.go index aa0424f8..c21da2aa 100644 --- a/internal/model/task_execution_test.go +++ b/internal/model/task_execution_test.go @@ -277,7 +277,7 @@ func TestListTaskExecutions(t *testing.T) { assert.Equal(t, "list_002", items[0].TaskID) // 按类型筛选 - items, total, err = ListTaskExecutions(ctx, ListTaskExecutionsRequest{TaskType: "other:task", Page: 1, PageSize: 10}) + _, total, err = ListTaskExecutions(ctx, ListTaskExecutionsRequest{TaskType: "other:task", Page: 1, PageSize: 10}) require.NoError(t, err) assert.Equal(t, int64(2), total) diff --git a/internal/storage/cache.go b/internal/storage/cache.go index 8e263493..45f4bc6c 100644 --- a/internal/storage/cache.go +++ b/internal/storage/cache.go @@ -106,12 +106,14 @@ func GetObjectViaCache(ctx context.Context, key string) (*ObjectInfo, error) { } func getLocalCacheFile(ctx context.Context, localPath, metaPath string) (*ObjectInfo, error) { - ctx, span := otel_trace.Start(ctx, "S3.GetLocalCacheFile", trace.WithSpanKind(trace.SpanKindClient)) + _, span := otel_trace.Start(ctx, "S3.GetLocalCacheFile", trace.WithSpanKind(trace.SpanKindClient)) defer span.End() // 尝试打开本地缓存文件 file, err := os.Open(localPath) - defer file.Close() + if err == nil { + defer func() { _ = file.Close() }() + } // 文件不存在 if err != nil && os.IsNotExist(err) { @@ -149,7 +151,7 @@ func getLocalCacheFile(ctx context.Context, localPath, metaPath string) (*Object } func saveToLocalCache(ctx context.Context, localPath, metaPath string, objInfo *ObjectInfo) error { - ctx, span := otel_trace.Start(ctx, "S3.SaveToLocalCache", trace.WithSpanKind(trace.SpanKindClient)) + _, span := otel_trace.Start(ctx, "S3.SaveToLocalCache", trace.WithSpanKind(trace.SpanKindClient)) defer span.End() // 创建目录 @@ -186,7 +188,7 @@ func saveFile(localPath string, data io.Reader) error { if err != nil { return err } - defer os.Remove(tempFile.Name()) + defer func() { _ = os.Remove(tempFile.Name()) }() // 将内容写入临时文件 if _, err := tempFile.ReadFrom(data); err != nil { diff --git a/internal/storage/s3.go b/internal/storage/s3.go index f41292ee..34a00417 100644 --- a/internal/storage/s3.go +++ b/internal/storage/s3.go @@ -227,7 +227,7 @@ func GetObjectViaProxy(ctx context.Context, key string) (*ObjectInfo, error) { } if resp.StatusCode != http.StatusOK { - resp.Body.Close() + _ = resp.Body.Close() span.SetStatus(codes.Error, fmt.Sprintf("cdn returned status %d", resp.StatusCode)) return nil, fmt.Errorf(errCDNStatusFailed, resp.StatusCode) } diff --git a/internal/task/executor.go b/internal/task/executor.go index aee277ec..2e6bb852 100644 --- a/internal/task/executor.go +++ b/internal/task/executor.go @@ -19,6 +19,7 @@ package task import ( "context" + "errors" "fmt" "time" @@ -155,7 +156,7 @@ func RetryTask(ctx context.Context, id uint64) (string, error) { } if !execution.Retryable { - return "", fmt.Errorf(errTaskNotRetryable) + return "", errors.New(errTaskNotRetryable) } if execution.RetryCount >= execution.MaxRetry { diff --git a/internal/task/executor_test.go b/internal/task/executor_test.go index 697a122b..b9484872 100644 --- a/internal/task/executor_test.go +++ b/internal/task/executor_test.go @@ -73,7 +73,7 @@ func setupTest(t *testing.T) func() { RegisterHandler(testTaskType, successHandler()) return func() { if AsynqClient != nil { - AsynqClient.Close() + _ = AsynqClient.Close() AsynqClient = nil } cleanup() diff --git a/internal/testhelper/test_helper.go b/internal/testhelper/test_helper.go index 2e67fd05..8f640dae 100644 --- a/internal/testhelper/test_helper.go +++ b/internal/testhelper/test_helper.go @@ -74,7 +74,7 @@ func SetupTestEnvironment(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) // Cleanup function cleanup := func() { - redisClient.Close() + _ = redisClient.Close() mr.Close() // Reset database and Redis references db.SetDB(nil) diff --git a/internal/util/cap/cap.go b/internal/util/cap/cap.go index acd5be50..d2cbd362 100644 --- a/internal/util/cap/cap.go +++ b/internal/util/cap/cap.go @@ -36,7 +36,7 @@ type ChallengeConfig struct { Count int // Number of puzzles (c) Size int // Salt length (s) Difficulty int // Difficulty prefix length (d) - ExpiresMs time.Duration // Challenge TTL + Expires time.Duration // Challenge TTL } // ChallengeResponse is returned to the client @@ -156,12 +156,12 @@ func GenerateChallenge(secret []byte, conf ChallengeConfig, scope string) (*Chal if conf.Difficulty <= 0 { conf.Difficulty = 4 } - if conf.ExpiresMs <= 0 { - conf.ExpiresMs = 10 * time.Minute + if conf.Expires <= 0 { + conf.Expires = 10 * time.Minute } now := time.Now().UnixNano() / int64(time.Millisecond) - expires := now + int64(conf.ExpiresMs/time.Millisecond) + expires := now + int64(conf.Expires/time.Millisecond) payload := ChallengePayload{ Nonce: randomHex(25), diff --git a/internal/util/cap/manager.go b/internal/util/cap/manager.go index 27d9f920..26968182 100644 --- a/internal/util/cap/manager.go +++ b/internal/util/cap/manager.go @@ -75,7 +75,7 @@ func (m *Manager) Generate(ctx context.Context, scope string) (*ChallengeRespons Count: m.getChallengeCount(ctx), Size: m.getChallengeSize(ctx), Difficulty: m.getChallengeDifficulty(ctx), - ExpiresMs: m.getChallengeTTL(ctx), + Expires: m.getChallengeTTL(ctx), } return GenerateChallenge(m.conf.Secret, c, scope) } @@ -190,14 +190,6 @@ func (m *Manager) VerifyToken(ctx context.Context, token string, expectedScope s return true, nil } -// sGet safely calls store.Get, treating a nil store as a miss. -func sGet(ctx context.Context, store Store, key string) (string, bool, error) { - if store == nil { - return "", false, nil - } - return store.Get(ctx, key) -} - // sGetAndDelete safely calls store.GetAndDelete, treating a nil store as a miss. func sGetAndDelete(ctx context.Context, store Store, key string) (string, bool, error) { if store == nil { diff --git a/internal/util/cap/prng.go b/internal/util/cap/prng.go index 6bd59242..aa21a61d 100644 --- a/internal/util/cap/prng.go +++ b/internal/util/cap/prng.go @@ -41,11 +41,6 @@ func fnv1aResume(state uint32, str string) uint32 { return h } -// prng generates a hex string of specified length using a seed -func prng(seed string, length int) string { - return prngFromHash(fnv1a(seed), length) -} - // prngFromHash generates a hex string of specified length using an initial hash state func prngFromHash(initialHash uint32, length int) string { state := initialHash diff --git a/internal/util/http_clients.go b/internal/util/http_clients.go index b18db98d..355cf6de 100644 --- a/internal/util/http_clients.go +++ b/internal/util/http_clients.go @@ -58,16 +58,12 @@ func Request(ctx context.Context, method, url string, body io.Reader, headers, c return nil, fmt.Errorf(errCreateHTTPRequestFailed, err) } - if cookies != nil { - for key, value := range cookies { - req.AddCookie(&http.Cookie{Name: key, Value: value}) - } + for key, value := range cookies { + req.AddCookie(&http.Cookie{Name: key, Value: value}) } - if headers != nil { - for key, value := range headers { - req.Header.Set(key, value) - } + for key, value := range headers { + req.Header.Set(key, value) } resp, err := httpClient.Do(req) diff --git a/internal/util/mail/mail.go b/internal/util/mail/mail.go index 02a0c0d3..019db862 100644 --- a/internal/util/mail/mail.go +++ b/internal/util/mail/mail.go @@ -71,14 +71,14 @@ func SendMailHTML(cfg Config, to string, subject, body string) error { if err != nil { return fmt.Errorf(errDialTLSFailed, err) } - defer conn.Close() + defer func() { _ = conn.Close() }() _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) client, err := smtp.NewClient(conn, cfg.Host) if err != nil { return fmt.Errorf(errSMTPClientCreationFailed, err) } - defer client.Close() + defer func() { _ = client.Close() }() if err = client.Auth(auth); err != nil { return fmt.Errorf(errSMTPAuthFailed, err) @@ -96,7 +96,7 @@ func SendMailHTML(cfg Config, to string, subject, body string) error { if err != nil { return fmt.Errorf(errSMTPDataCommandFailed, err) } - defer w.Close() + defer func() { _ = w.Close() }() _, err = w.Write([]byte(message)) if err != nil { @@ -119,7 +119,7 @@ func SendMailHTML(cfg Config, to string, subject, body string) error { func SendMailWithLog(cfg Config, to string, subject, body string) (string, error) { var logBuf bytes.Buffer logLine := func(dir string, format string, args ...interface{}) { - logBuf.WriteString(fmt.Sprintf("[%s] %s\n", dir, fmt.Sprintf(format, args...))) + fmt.Fprintf(&logBuf, "[%s] %s\n", dir, fmt.Sprintf(format, args...)) } addr := net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)) @@ -141,7 +141,7 @@ func SendMailWithLog(cfg Config, to string, subject, body string) (string, error logLine("Error", "Connection failed: %v", err) return logBuf.String(), err } - defer conn.Close() + defer func() { _ = conn.Close() }() logLine("System", "Connected successfully.") // Set a 10-second session deadline for read/write operations @@ -152,7 +152,7 @@ func SendMailWithLog(cfg Config, to string, subject, body string) (string, error logLine("Error", "SMTP client handshake failed: %v", err) return logBuf.String(), err } - defer client.Close() + defer func() { _ = client.Close() }() // If not 465, support STARTTLS if available if cfg.Port != 465 { @@ -222,11 +222,11 @@ func SendMailWithLog(cfg Config, to string, subject, body string) (string, error logLine("System", "Sending message body...") if _, err = w.Write([]byte(message)); err != nil { - w.Close() + _ = w.Close() logLine("Error", "Writing message body failed: %v", err) return logBuf.String(), err } - w.Close() + _ = w.Close() logLine("S", "250 OK") logLine("C", "QUIT") diff --git a/internal/util/mail/mail_test.go b/internal/util/mail/mail_test.go index 50b9df8e..26f0297f 100644 --- a/internal/util/mail/mail_test.go +++ b/internal/util/mail/mail_test.go @@ -30,7 +30,7 @@ func TestSendMailMock(t *testing.T) { if err != nil { t.Fatalf("failed to start mock smtp server: %v", err) } - defer l.Close() + defer func() { _ = l.Close() }() port := l.Addr().(*net.TCPAddr).Port @@ -39,7 +39,7 @@ func TestSendMailMock(t *testing.T) { if err != nil { return } - defer conn.Close() + defer func() { _ = conn.Close() }() writer := bufio.NewWriter(conn) reader := bufio.NewReader(conn)