From 4c78ab6cc8350f6cf812240ebfd8ab8f7e9eb2c3 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Fri, 24 Apr 2026 00:19:56 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=96=B0=E5=A2=9E=20backend-code-lint?= =?UTF-8?q?=20=E5=8F=98=E6=9B=B4=E8=AE=A1=E5=88=92=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../changes/backend-code-lint/.openspec.yaml | 2 + openspec/changes/backend-code-lint/design.md | 130 +++++++++++++ .../changes/backend-code-lint/proposal.md | 29 +++ .../backend-code-lint/specs/code-lint/spec.md | 174 ++++++++++++++++++ .../specs/error-handling/spec.md | 45 +++++ .../specs/module-logging/spec.md | 32 ++++ .../specs/pre-commit-hook/spec.md | 54 ++++++ .../specs/structured-logging/spec.md | 30 +++ openspec/changes/backend-code-lint/tasks.md | 42 +++++ 10 files changed, 539 insertions(+) create mode 100644 openspec/changes/backend-code-lint/.openspec.yaml create mode 100644 openspec/changes/backend-code-lint/design.md create mode 100644 openspec/changes/backend-code-lint/proposal.md create mode 100644 openspec/changes/backend-code-lint/specs/code-lint/spec.md create mode 100644 openspec/changes/backend-code-lint/specs/error-handling/spec.md create mode 100644 openspec/changes/backend-code-lint/specs/module-logging/spec.md create mode 100644 openspec/changes/backend-code-lint/specs/pre-commit-hook/spec.md create mode 100644 openspec/changes/backend-code-lint/specs/structured-logging/spec.md create mode 100644 openspec/changes/backend-code-lint/tasks.md diff --git a/.gitignore b/.gitignore index bff243e..932c0fa 100644 --- a/.gitignore +++ b/.gitignore @@ -401,6 +401,7 @@ cython_debug/ # Custom .claude .opencode +.codex openspec/changes/archive temp .agents diff --git a/openspec/changes/backend-code-lint/.openspec.yaml b/openspec/changes/backend-code-lint/.openspec.yaml new file mode 100644 index 0000000..8b394c6 --- /dev/null +++ b/openspec/changes/backend-code-lint/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-23 diff --git a/openspec/changes/backend-code-lint/design.md b/openspec/changes/backend-code-lint/design.md new file mode 100644 index 0000000..2d8092b --- /dev/null +++ b/openspec/changes/backend-code-lint/design.md @@ -0,0 +1,130 @@ +## Context + +后端项目使用 Go 1.26.2 开发,已集成 `golangci-lint v1.64.8` 作为 tool dependency(`go.mod` 中声明),并通过 `make backend-lint` 调用。但当前**没有 `.golangci.yml` 配置文件**,lint 以默认配置运行(仅启用极少量 linter),且存在 embedfs 模块加载错误导致 lint 实际无法执行。 + +代码审计发现以下存量问题: +- 8 处 `err == sentinel` 应使用 `errors.Is()` +- 7 处 `_ = json.Marshal(...)` 忽略错误返回值 +- 13 处 `zap.String("error", err.Error())` 应使用 `zap.Error(err)` +- 2 处 `fmt.Fprintf(os.Stderr, ...)` 应走 logger +- 1 处 `_ = io.ReadAll(...)` 忽略错误 + +## Goals / Non-Goals + +**Goals:** +- 配置 golangci-lint,将 README 中的编码规范转化为机器可检查的硬约束 +- 引入 lefthook 实现 pre-commit 自动 lint,AI 提交代码时自动拦截违规 +- 修复存量代码中的规范违规 +- 解决 embedfs 导致 lint 无法运行的阻塞问题 + +**Non-Goals:** +- 不引入自定义 linter 插件(开发成本过高) +- 不配置 CI pipeline lint 门禁(仅本地) +- 不改变现有错误响应策略(允许 err.Error() 暴露在 HTTP 响应中) +- 不引入 funlen linter(gocyclo 已控制复杂度,funlen 误报率高) +- 不引入 unparam linter(项目 interface 密集,unparam 误报率高) + +## Decisions + +### D1: Linter 选型 — 13 个 linter 分四层 + +``` +🔒 硬约束层(项目规范 → 机器检查) +├── forbidigo 禁止 fmt.Print*/log.*/zap.L()/zap.S() +├── errorlint 强制 errors.Is/As,禁止 err == 比较 +└── errcheck 禁止忽略错误返回值(check-blank: true) + +🏗️ 质量基线层 +├── staticcheck Go 团队官方综合静态分析 +├── revive golint 替代品,精选 8 条规则 +│ (exported, var-naming, indent-error-flow, error-strings, +│ error-return, blank-imports, context-as-argument, +│ unexported-return) +├── gocritic 100+ 代码质量规则 +└── gosec 安全检查 + +🛡️ 资源安全层 +├── bodyclose HTTP 响应 Body 关闭检查 +├── noctx HTTP 请求必须携带 context +└── nilerr 检查 if err != nil { return nil } 遗漏 + +📐 格式层(可自动修复) +├── gofumpt gofmt 的严格版 +└── goimports import 排序(local-prefixes: nex/backend) + +📊 复杂度 +└── gocyclo 正式代码 ≤10 / 测试代码 ≤20 +``` + +**替代方案**: 使用 revive 自定义规则检查 zap.String("error", err.Error()) → 决定暂不实施,先修复存量 13 处 + README 约定 + code review,如果后续频繁违规再投入开发自定义规则。 + +### D2: forbidigo 配置策略 + +禁止列表: +- `fmt\.Print*` — 必须使用 zap logger +- `fmt\.Fprint*(os\.(Stdout|Stderr)` — 必须使用 zap logger +- `log\.(Print|Fatal|Panic|Printf)*` — 必须使用 zap logger +- `zap\.L()` — 必须通过 DI 注入 *zap.Logger +- `zap\.S()` — 不使用 Sugar logger + +**不禁止**: +- `zap.Logger.Fatal()` — main() 中的 Fatal 是合理的启动终止模式,且 forbidigo 按函数匹配不会拦截 zap.Logger 的方法调用 +- `fmt.Sprintf` — 格式化字符串是合法用途 +- `fmt.Errorf` — 创建带格式的错误是标准用法 + +### D3: 测试代码 vs 正式代码差异化规则 + +| 规则 | 正式代码 | 测试代码 | +|---|---|---| +| forbidigo | 全部启用 | 放宽(fmt.Sprintf 等合理) | +| errcheck | check-blank: true | 放宽 check-blank | +| revive (exported) | 启用 | 排除 | +| gosec | 启用 | 排除 G101/G401/G501 | +| gocyclo | ≤10 | ≤20 | + +通过 `issues.exclude-rules` 按路径 `*_test.go` 和 `tests/` 配置排除。 + +### D4: 生成代码排除 + +`tests/mocks/` 目录下的 8 个 mock 文件由 mockgen 生成,需要排除。使用 `issues.exclude-dirs` 配合 `issues.exclude-generated: true`(golangci-lint 自动检测 `Code generated by` 标记)。 + +### D5: embedfs 阻塞问题修复 + +embedfs 模块使用 `//go:embed assets/*` 和 `//go:embed frontend-dist/*`,但这些目录在未构建时不存在,导致 golangci-lint 无法加载该模块。 + +**方案**: 在 `embedfs/assets/` 和 `embedfs/frontend-dist/` 中添加 `.gitkeep` 文件,使 `go:embed` 指令能匹配到内容。这是最小侵入的解决方案,不影响正常构建流程(desktop-build 会覆盖这些目录)。 + +### D6: lefthook pre-commit 配置 + +```yaml +# lefthook.yml +pre-commit: + commands: + backend-lint: + glob: "backend/**/*.go" + run: cd backend && go tool golangci-lint run --new-from-rev HEAD {staged_files} +``` + +关键设计: +- 只检查 staged 文件(`--new-from-rev HEAD`),速度快 +- 只在 Go 文件变更时触发(`glob: "backend/**/*.go"`) +- AI commit 时自动触发,lint 不过则 commit 被拒绝,形成自动反馈循环 + +### D7: 存量修复策略 + +按优先级分批修复: +1. **P0 — embedfs 阻塞修复**:创建 .gitkeep 文件 +2. **P1 — err == sentinel → errors.Is()**:8 处,分布在 handler 和 client +3. **P2 — 忽略错误返回值**:7 处 json.Marshal + 1 处 io.ReadAll + 2 处 stats Record(加 nolint 注释) +4. **P3 — zap.String("error", err.Error()) → zap.Error(err)**:13 处 +5. **P4 — fmt.Fprintf(os.Stderr) → logger**:2 处,在 cmd/desktop/ + +## Risks / Trade-offs + +**[lint 速度影响 commit 体验]** → lefthook 只检查 staged Go 文件,增量检查通常 <5 秒,可接受。如果仍慢,可加 `--timeout` 限制。 + +**[lefthook 是新依赖]** → lefthook 是开发工具依赖,不影响生产代码。作为单二进制分发,安装简单(`go install` 或从 GitHub release 下载)。首次需要开发者手动安装。 + +**[存量修复可能引入新 bug]** → 所有修复都是机械性替换(errors.Is、zap.Error 等),不改变逻辑。修复后运行 `make test` 确认无回归。 + +**[forbidigo 可能误拦合理用法]** → 通过仔细配置允许列表(允许 fmt.Sprintf、fmt.Errorf),并在发现误报时调整规则。 diff --git a/openspec/changes/backend-code-lint/proposal.md b/openspec/changes/backend-code-lint/proposal.md new file mode 100644 index 0000000..89f0669 --- /dev/null +++ b/openspec/changes/backend-code-lint/proposal.md @@ -0,0 +1,29 @@ +## Why + +项目复杂度增长后,AI 编写代码时经常忽略基本编码规范(如使用指定日志工具、正确处理错误等)。依赖 prompt 约定是"软约束",无法可靠防止违规。需要引入静态分析工具,将编码规范从"约定"升级为"机器可检查的硬约束",在提交时自动拦截问题代码。 + +## What Changes + +- 新增 `.golangci.yml` 配置文件,启用 13 个 linter 并配置项目专属规则 +- 引入 lefthook 作为 Git hook 管理器,在 pre-commit 时自动运行 lint +- 修复存量代码中的规范违规(约 31 处) +- 解决 embedfs 模块导致 golangci-lint 无法运行的阻塞问题 +- 更新 README.md 补充代码规范说明 + +## Capabilities + +### New Capabilities +- `code-lint`: 后端代码静态分析规则配置,包括 13 个 linter 的启用、参数配置、测试/正式代码的差异化规则、生成代码排除等 +- `pre-commit-hook`: 基于 lefthook 的 pre-commit hook 配置,提交时自动运行 lint 检查 + +### Modified Capabilities +- `module-logging`: 新增 zap.Error(err) 优于 zap.String("error", err.Error()) 的规范要求 +- `error-handling`: 新增必须使用 errors.Is/As 而非直接 == 比较的强制要求 +- `structured-logging`: 补充 zap.Error(err) 的使用约定 + +## Impact + +- 新增开发依赖:lefthook(二进制工具,不影响生产代码) +- 修改文件:约 15 个 Go 源文件(存量修复)、README.md、Makefile +- 新增文件:`.golangci.yml`、`lefthook.yml`、`embedfs/assets/.gitkeep`、`embedfs/frontend-dist/.gitkeep` +- 开发流程影响:git commit 时自动触发 lint 检查,lint 不过则提交被拒绝 diff --git a/openspec/changes/backend-code-lint/specs/code-lint/spec.md b/openspec/changes/backend-code-lint/specs/code-lint/spec.md new file mode 100644 index 0000000..53cba5c --- /dev/null +++ b/openspec/changes/backend-code-lint/specs/code-lint/spec.md @@ -0,0 +1,174 @@ +# Code Lint + +## Purpose + +定义后端 Go 代码静态分析规则,将编码规范从人工约定升级为机器可检查的硬约束,通过 golangci-lint 在开发和提交阶段自动拦截违规代码。 + +## ADDED Requirements + +### Requirement: golangci-lint 配置 + +系统 SHALL 通过 `.golangci.yml` 配置 golangci-lint,启用 13 个 linter。 + +#### Scenario: 配置文件位置 + +- **WHEN** 配置 lint 规则 +- **THEN** 配置文件 SHALL 位于 `backend/.golangci.yml` + +#### Scenario: 启用的 linter 列表 + +- **WHEN** 运行 golangci-lint +- **THEN** SHALL 启用以下 linter:forbidigo、errorlint、errcheck、staticcheck、revive、gocritic、gosec、bodyclose、noctx、nilerr、gofumpt、goimports、gocyclo + +### Requirement: forbidigo 日志输出约束 + +系统 SHALL 通过 forbidigo 禁止在正式代码中使用直接输出函数。 + +#### Scenario: 禁止 fmt.Print 系列 + +- **WHEN** 正式代码中调用 fmt.Print、fmt.Println、fmt.Printf +- **THEN** lint SHALL 报错,提示使用 zap logger + +#### Scenario: 禁止 fmt.Fprint 到 Stdout/Stderr + +- **WHEN** 正式代码中调用 fmt.Fprintf(os.Stdout, ...) 或 fmt.Fprintf(os.Stderr, ...) +- **THEN** lint SHALL 报错,提示使用 zap logger + +#### Scenario: 禁止标准库 log + +- **WHEN** 正式代码中调用 log.Print、log.Fatal、log.Panic、log.Printf 等 +- **THEN** lint SHALL 报错,提示使用 zap logger + +#### Scenario: 禁止 zap.L() 全局 logger + +- **WHEN** 正式代码中调用 zap.L() +- **THEN** lint SHALL 报错,提示通过 DI 注入 *zap.Logger + +#### Scenario: 禁止 zap.S() Sugar logger + +- **WHEN** 代码中调用 zap.S() +- **THEN** lint SHALL 报错,不使用 Sugar logger + +#### Scenario: 允许 fmt.Sprintf 和 fmt.Errorf + +- **WHEN** 代码中使用 fmt.Sprintf 或 fmt.Errorf +- **THEN** lint SHALL NOT 报错 + +#### Scenario: 测试代码放宽 + +- **WHEN** 测试文件(*_test.go)或 tests/ 目录中使用 fmt.Print 系列 +- **THEN** forbidigo SHALL NOT 报错 + +### Requirement: errorlint 错误比较约束 + +系统 SHALL 通过 errorlint 强制使用类型安全的错误比较方式。 + +#### Scenario: 禁止 err == sentinel 比较 + +- **WHEN** 代码中使用 `err == someError` 直接比较错误 +- **THEN** lint SHALL 报错,要求使用 errors.Is() + +#### Scenario: 禁止直接类型断言 + +- **WHEN** 代码中使用 `err.(SomeType)` 直接类型断言 +- **THEN** lint SHALL 报错,要求使用 errors.As() + +### Requirement: errcheck 错误返回值检查 + +系统 SHALL 通过 errcheck 禁止忽略函数返回的错误。 + +#### Scenario: 启用 check-blank + +- **WHEN** 代码中使用 `_ = someFuncReturnsError()` +- **THEN** lint SHALL 报错(除非排除列表中的函数) + +#### Scenario: 启用 check-type-assertions + +- **WHEN** 代码中使用未检查的类型断言 `v := x.(Type)` +- **THEN** lint SHALL 报错 + +#### Scenario: 排除 fmt.Fprintf + +- **WHEN** 代码中忽略 fmt.Fprintf 的返回值 +- **THEN** errcheck SHALL NOT 报错(io.Writer 场景合理) + +#### Scenario: 测试代码放宽 + +- **WHEN** 测试文件中忽略错误返回值 +- **THEN** errcheck 的 check-blank SHALL 放宽 + +### Requirement: revive 代码风格规则 + +系统 SHALL 通过 revive 启用精选的 8 条代码风格规则。 + +#### Scenario: 启用的规则 + +- **WHEN** 运行 revive +- **THEN** SHALL 启用:exported、var-naming、indent-error-flow、error-strings、error-return、blank-imports、context-as-argument、unexported-return + +#### Scenario: 测试代码排除 exported + +- **WHEN** 测试文件中的导出符号缺少文档注释 +- **THEN** revive SHALL NOT 报错 + +### Requirement: gosec 安全检查 + +系统 SHALL 通过 gosec 检查常见安全问题。 + +#### Scenario: 正式代码全部启用 + +- **WHEN** 正式代码中存在安全隐患(硬编码凭证、SQL 注入等) +- **THEN** gosec SHALL 报错 + +#### Scenario: 测试代码排除部分规则 + +- **WHEN** 测试文件中触发 G101(硬编码密钥)、G401/G501(弱密码算法) +- **THEN** gosec SHALL NOT 报错 + +### Requirement: gocyclo 圈复杂度控制 + +系统 SHALL 通过 gocyclo 控制函数复杂度。 + +#### Scenario: 正式代码复杂度阈值 + +- **WHEN** 正式代码中函数圈复杂度超过 10 +- **THEN** gocyclo SHALL 报错 + +#### Scenario: 测试代码复杂度阈值 + +- **WHEN** 测试代码中函数圈复杂度超过 20 +- **THEN** gocyclo SHALL 报错 + +### Requirement: goimports import 排序 + +系统 SHALL 通过 goimports 统一 import 分组排序。 + +#### Scenario: 三组格式 + +- **WHEN** 格式化 import +- **THEN** SHALL 按标准库、第三方库、本地包(nex/backend)三组排序 +- **THEN** local-prefixes SHALL 配置为 nex/backend + +### Requirement: 生成代码排除 + +系统 SHALL 排除自动生成的代码的 lint 检查。 + +#### Scenario: mocks 目录排除 + +- **WHEN** lint 扫描 tests/mocks/ 目录 +- **THEN** SHALL 排除该目录(由 mockgen 生成的代码) + +#### Scenario: Code generated 标记自动检测 + +- **WHEN** 文件包含 `// Code generated by` 标记 +- **THEN** golangci-lint SHALL 自动排除该文件 + +### Requirement: embedfs 编译兼容 + +系统 SHALL 确保 golangci-lint 能正常加载 embedfs 模块。 + +#### Scenario: 空目录占位 + +- **WHEN** embedfs 模块的 assets/ 和 frontend-dist/ 目录不存在 +- **THEN** SHALL 通过 .gitkeep 文件确保目录存在 +- **THEN** go:embed 指令 SHALL 能正常匹配 diff --git a/openspec/changes/backend-code-lint/specs/error-handling/spec.md b/openspec/changes/backend-code-lint/specs/error-handling/spec.md new file mode 100644 index 0000000..8ef7fea --- /dev/null +++ b/openspec/changes/backend-code-lint/specs/error-handling/spec.md @@ -0,0 +1,45 @@ +# Error Handling — Delta + +## MODIFIED Requirements + +### Requirement: 使用类型安全错误判断 + +系统 SHALL 使用类型安全方式判断错误类型,并通过 lint 工具强制执行。 + +#### Scenario: 数据库错误判断 + +- **WHEN** 判断数据库唯一约束错误 +- **THEN** SHALL 使用 errors.Is(err, gorm.ErrDuplicatedKey) +- **THEN** SHALL NOT 使用字符串匹配 err.Error() + +#### Scenario: 网络错误判断 + +- **WHEN** 判断网络错误 +- **THEN** SHALL 使用 errors.As(err, &net.Error) 判断网络错误 +- **THEN** SHALL 使用 errors.As(err, &net.OpError) 判断操作错误 +- **THEN** SHALL 使用 errors.Is(opErr.Err, syscall.ECONNRESET) 判断连接重置 +- **THEN** SHALL NOT 使用字符串匹配判断错误类型 + +#### Scenario: 错误链判断 + +- **WHEN** 判断错误链中的特定错误 +- **THEN** SHALL 使用 errors.Is 进行链式判断 +- **THEN** SHALL 使用 errors.As 提取特定类型错误 + +#### Scenario: lint 自动拦截错误比较 + +- **WHEN** 代码中使用 `err == someError` 直接比较 +- **THEN** errorlint SHALL 检测并报错 +- **THEN** SHALL 改用 errors.Is() + +#### Scenario: lint 自动拦截类型断言 + +- **WHEN** 代码中使用 `err.(SomeType)` 直接类型断言 +- **THEN** errorlint SHALL 检测并报错 +- **THEN** SHALL 改用 errors.As() + +#### Scenario: lint 自动拦截忽略错误返回值 + +- **WHEN** 代码中使用 `_ = funcReturnsError()` 忽略错误 +- **THEN** errcheck SHALL 检测并报错 +- **THEN** SHALL 正确处理错误或添加 //nolint:errcheck 注释(仅在有意忽略时) diff --git a/openspec/changes/backend-code-lint/specs/module-logging/spec.md b/openspec/changes/backend-code-lint/specs/module-logging/spec.md new file mode 100644 index 0000000..7458781 --- /dev/null +++ b/openspec/changes/backend-code-lint/specs/module-logging/spec.md @@ -0,0 +1,32 @@ +# Module Logging — Delta + +## MODIFIED Requirements + +### Requirement: 禁止全局 logger + +系统 SHALL 禁止在业务代码中使用全局 logger,并通过 lint 工具强制执行。 + +#### Scenario: 移除 zap.L() 调用 + +- **WHEN** 重构现有代码 +- **THEN** SHALL 移除所有 `zap.L()` 调用 +- **THEN** SHALL 通过构造函数注入 logger +- **THEN** 允许仅在测试代码中使用 `zap.L()` 或 `zap.NewNop()` + +#### Scenario: 移除 zap.L() fallback + +- **WHEN** 构造函数 logger 参数为 nil +- **THEN** SHALL NOT 使用 `zap.L()` 作为默认值 +- **THEN** 调用方 SHALL 必须传入有效的 logger + +#### Scenario: lint 自动拦截 zap.L() + +- **WHEN** 正式代码中新增 `zap.L()` 调用 +- **THEN** forbidigo SHALL 检测并报错 +- **THEN** git commit SHALL 被拒绝 + +#### Scenario: 禁止 fmt/os.Stderr 直接输出 + +- **WHEN** 正式代码中使用 fmt.Print*、fmt.Fprintf(os.Stderr, ...) 等直接输出 +- **THEN** forbidigo SHALL 检测并报错 +- **THEN** SHALL 使用注入的 zap logger 替代 diff --git a/openspec/changes/backend-code-lint/specs/pre-commit-hook/spec.md b/openspec/changes/backend-code-lint/specs/pre-commit-hook/spec.md new file mode 100644 index 0000000..8a489eb --- /dev/null +++ b/openspec/changes/backend-code-lint/specs/pre-commit-hook/spec.md @@ -0,0 +1,54 @@ +# Pre-commit Hook + +## Purpose + +定义基于 lefthook 的 pre-commit hook 配置,在 git commit 时自动运行 lint 检查,拦截违规代码提交。 + +## ADDED Requirements + +### Requirement: lefthook 配置 + +系统 SHALL 通过 `lefthook.yml` 配置 pre-commit hook。 + +#### Scenario: 配置文件位置 + +- **WHEN** 配置 lefthook +- **THEN** 配置文件 SHALL 位于项目根目录 `lefthook.yml` + +#### Scenario: pre-commit hook 安装 + +- **WHEN** 开发者首次克隆项目 +- **THEN** 运行 `lefthook install` SHALL 安装 git hooks +- **THEN** hooks SHALL 自动注册到 .git/hooks/ + +### Requirement: Go 文件变更触发 lint + +系统 SHALL 在 Go 文件变更时自动运行 golangci-lint。 + +#### Scenario: 检测到 Go 文件变更 + +- **WHEN** git commit 中包含 backend/**/*.go 文件的变更 +- **THEN** SHALL 自动运行 golangci-lint + +#### Scenario: 增量检查 + +- **WHEN** 运行 lint +- **THEN** SHALL 只检查 staged 文件(使用 --new-from-rev HEAD) +- **THEN** SHALL NOT 检查整个代码库 + +#### Scenario: lint 通过 + +- **WHEN** golangci-lint 检查通过 +- **THEN** commit SHALL 正常完成 + +#### Scenario: lint 失败 + +- **WHEN** golangci-lint 检查发现违规 +- **THEN** commit SHALL 被拒绝 +- **THEN** SHALL 显示具体的违规信息和修复建议 + +#### Scenario: 无 Go 文件变更 + +- **WHEN** git commit 不包含 Go 文件变更 +- **THEN** SHALL NOT 运行 golangci-lint +- **THEN** commit SHALL 正常完成 diff --git a/openspec/changes/backend-code-lint/specs/structured-logging/spec.md b/openspec/changes/backend-code-lint/specs/structured-logging/spec.md new file mode 100644 index 0000000..38df8bd --- /dev/null +++ b/openspec/changes/backend-code-lint/specs/structured-logging/spec.md @@ -0,0 +1,30 @@ +# Structured Logging — Delta + +## MODIFIED Requirements + +### Requirement: 字段标准化 + +系统 SHALL 使用标准化字段定义,并通过 lint 工具强制执行错误字段规范。 + +#### Scenario: 标准字段常量 + +- **WHEN** 记录日志字段 +- **THEN** SHALL 使用 `pkg/logger/field.go` 中定义的常量 +- **THEN** 字段名 SHALL 包括:`request_id`、`provider_id`、`model_name`、`method`、`path`、`status`、`latency` + +#### Scenario: 错误字段统一 + +- **WHEN** 记录错误日志 +- **THEN** SHALL 使用 `zap.Error(err)` +- **THEN** SHALL NOT 使用 `zap.String("error", err.Error())` + +#### Scenario: lint 强化错误字段约束 + +- **WHEN** 存量代码中使用 `zap.String("error", err.Error())` 记录错误 +- **THEN** SHALL 修改为 `zap.Error(err)` + +#### Scenario: 字段构造函数 + +- **WHEN** 构造日志字段 +- **THEN** SHALL 优先使用 `pkg/logger` 提供的辅助函数 +- **THEN** 辅助函数 SHALL 返回 `zap.Field` 类型 diff --git a/openspec/changes/backend-code-lint/tasks.md b/openspec/changes/backend-code-lint/tasks.md new file mode 100644 index 0000000..dda1fe3 --- /dev/null +++ b/openspec/changes/backend-code-lint/tasks.md @@ -0,0 +1,42 @@ +## 1. 基础设施 + +- [x] 1.1 创建 embedfs/assets/.gitkeep 和 embedfs/frontend-dist/.gitkeep,解决 embedfs 编译阻塞 +- [x] 1.2 创建 backend/.golangci.yml 配置文件,启用 13 个 linter 并配置所有规则(forbidigo、errorlint、errcheck、staticcheck、revive、gocritic、gosec、bodyclose、noctx、nilerr、gofumpt、goimports、gocyclo) +- [x] 1.3 配置 .golangci.yml 中测试代码差异化规则(exclude-rules for *_test.go and tests/) +- [x] 1.4 配置 .golangci.yml 排除生成代码(exclude-dirs: tests/mocks, exclude-generated: true) +- [ ] 1.5 运行 make backend-lint 验证配置可正常执行(无 embedfs 报错) +- [x] 1.6 创建 lefthook.yml 配置文件,配置 pre-commit hook 仅检查 staged Go 文件 +- [ ] 1.7 运行 lefthook install 安装 git hooks 并验证 hook 生效 + +## 2. 存量代码修复 — 错误比较 + +- [x] 2.1 修复 internal/handler/model_handler.go 中 4 处 err == sentinel → errors.Is() +- [x] 2.2 修复 internal/handler/provider_handler.go 中 4 处 err == sentinel → errors.Is() +- [x] 2.3 修复 internal/provider/client.go:223 err == io.EOF → errors.Is(err, io.EOF) + +## 3. 存量代码修复 — 忽略错误返回值 + +- [x] 3.1 修复 internal/conversion/openai/adapter.go 中 3 处 _ = json.Marshal → 正确处理错误 +- [x] 3.2 修复 internal/conversion/anthropic/adapter.go 中 2 处 _ = json.Marshal → 正确处理错误 +- [x] 3.3 修复 internal/conversion/anthropic/decoder.go 中 1 处 _ = json.Marshal → 正确处理错误 +- [x] 3.4 修复 internal/conversion/engine.go:394 _ = json.Marshal → 正确处理错误(fallback 场景) +- [x] 3.5 修复 internal/provider/client.go:144 _ = io.ReadAll → 正确处理错误 +- [x] 3.6 为 internal/handler/proxy_handler.go 中 2 处 _ = statsService.Record 添加 //nolint:errcheck 注释(goroutine fire-and-forget 模式) + +## 4. 存量代码修复 — 日志字段 + +- [x] 4.1 修复 internal/handler/proxy_handler.go 中 zap.String("error", err.Error()) → zap.Error(err)(约 6 处) +- [x] 4.2 修复 internal/provider/client.go:187 zap.String("error", err.Error()) → zap.Error(err) +- [x] 4.3 修复 internal/conversion/engine.go 中 zap.String("error", err.Error()) → zap.Error(err)(约 6 处) + +## 5. 存量代码修复 — 桌面端日志 + +- [x] 5.1 修复 cmd/desktop/dialog_linux.go 中 2 处 fmt.Fprintf(os.Stderr, ...) → 改用 zap logger + +## 6. 验证与文档 + +- [ ] 6.1 运行 make backend-lint 确认所有 linter 通过 +- [ ] 6.2 运行 make backend-test 确认所有测试通过 +- [x] 6.3 更新 backend/README.md 编码规范部分:补充 zap.Error(err) 优先于 zap.String("error", err.Error()) 的规范 +- [x] 6.4 更新 backend/README.md 编码规范部分:补充强制使用 errors.Is/As 而非 == 比较的说明 +- [x] 6.5 更新 README.md 添加 lefthook 安装说明(首次克隆项目后需执行 lefthook install)