refactor: 后端代码优化 11 项(codestable/refactors/2026-04-25-backend-cleanup)

- 提取 getWKFromContext 辅助函数,消除 handler 中 5 处重复代码
- 提取 retryCode 函数,消除 Login/performStudy 中验证码重试重复
- 提取 removeSession 内部方法,消除 Del/ClearAll/ClearExpired 中 3 处重复
- 提取 WK.UserKey() 方法,消除 4 处 userKey 手动拼接
- SessionManager.Get() 改用 RLock 优化读性能
- GetRecords 递归分页改为迭代,避免栈溢出
- prepareRequestClient 添加配置缓存,仅在 debug 设置变化时重建
- 修正 schedule.go 时区为 Asia/Shanghai + cron "0 6 * * *"
- 修正 typo "以达到" → "已达到"
- 删除未使用的 QAList struct
- 修复 bufferHub.append 切片内存泄漏

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 13:07:45 +08:00
parent 2a6732ffe7
commit 536aa506f9
9 changed files with 488 additions and 94 deletions

View File

@@ -0,0 +1,76 @@
---
doc_type: refactor-apply-notes
refactor: 2026-04-25-backend-cleanup
---
# backend-cleanup apply notes
## 步骤 1: 删除未使用的 QAList struct#10
- 完成时间: 2026-04-26
- 改动文件: internal/ckwk/types.go
- 验证结果: go vet 通过go build 通过
- 偏离: 无
## 步骤 2: 修正 typo "以达到" → "已达到"#9
- 完成时间: 2026-04-26
- 改动文件: internal/ckwk/api.go (2 处)
- 验证结果: go build 通过
- 偏离: 无
## 步骤 3: 修正 schedule.go 时区和 cron 表达式(#8
- 完成时间: 2026-04-26
- 改动文件: internal/schedule/schedule.go
- 验证结果: go build 通过
- 偏离: 改为 Asia/Shanghai + "0 6 * * *" 对齐注释"每天 6 点执行"
## 步骤 4: 提取 userKey 构造为 WK.UserKey() 方法(#4
- 完成时间: 2026-04-26
- 改动文件: internal/ckwk/api.go, internal/ckwk/session_manager.go
- 验证结果: go vet 通过grep 确认无残留的 Host+":"+Username 拼接
- 偏离: 无
## 步骤 5: 提取 getWKFromContext 辅助函数(#1
- 完成时间: 2026-04-26
- 改动文件: internal/handler/ckwk.go
- 验证结果: go vet 通过grep 确认无残留的 ctx.Get("wk_instance") 模式(除 helper 函数本身外)
- 偏离: 无
## 步骤 6: 提取 retryCode 函数(#2
- 完成时间: 2026-04-26
- 改动文件: internal/ckwk/api.go
- 验证结果: go vet 通过
- 偏离: Login() 中原错误信息包含"登录终止"后缀,提取后统一为"已达到最大重试次数,验证码获取失败"(两个调用点共享同一个错误消息),不再区分"登录终止"和 Study 场景的措辞差异
## 步骤 7: 提取 removeSession 内部方法(#3
- 完成时间: 2026-04-26
- 改动文件: internal/ckwk/session_manager.go
- 验证结果: go vet 通过
- 偏离: 无
## 步骤 8: SessionManager.Get() 优化锁策略(#5
- 完成时间: 2026-04-26
- 改动文件: internal/ckwk/session_manager.go
- 验证结果: go vet 通过
- 偏离: 采用 RLock→检查存在→RUnlock→Lock→更新→Unlock 两步走方案
## 步骤 9: GetRecords 递归改迭代(#6
- 完成时间: 2026-04-26
- 改动文件: internal/ckwk/types.goGetRecords 从 resp.go 迁移至 types.go此文件原本就包含该函数
- 验证结果: go vet 通过
- 偏离: 提取了 fetchRecordPage 辅助函数来封装单页请求逻辑GetRecords 改为 for 循环迭代
## 步骤 10: prepareRequestClient 缓存配置(#7
- 完成时间: 2026-04-26
- 改动文件: internal/ckwk/api.go
- 验证结果: go vet 通过
- 偏离: 在 WK struct 上添加了 3 个缓存字段lastDebugEnabled, lastDebugProxy, lastDebugSkipSSLVerifyprepareRequestClient 在配置未变化时直接返回
## 步骤 11: 修复 bufferHub.append 切片内存泄漏(#12
- 完成时间: 2026-04-26
- 改动文件: pkg/log/buffer.go
- 验证结果: go vet 通过go build 通过
- 偏离: 无
## 全量验证
- go vet ./... 通过
- go build ./... 通过

View File

@@ -0,0 +1,128 @@
---
doc_type: refactor-design
refactor: 2026-04-25-backend-cleanup
status: approved
scope: 后端 Go 代码全部internal/、pkg/、cmd/21 文件 ~2485 行
summary: 执行 11 条低中风险重构6 条提取函数消除重复、2 条性能修复递归改迭代、锁优化、3 条小修typo、删除死代码、内存泄漏
---
# backend-cleanup refactor design
## 1. 本次范围
- 勾选:#1 #2 #3 #4 #5 #6 #7 #8 #9 #10 #12
- 明确不做:#11Handler 层业务逻辑下沉——高风险、跨模块,建议单独开一轮 refactor
- 预估总工作量:小 / 总风险:低
## 2. 前置依赖
- 无需补测试(本次全部为纯提取/等价替换,不改变控制流)
- #8 需要用户确认业务意图(北京时间 6 点 vs 新加坡时间凌晨 2 点)
## 3. 执行顺序
### 步骤 1删除未使用的 QAList struct#10
- 引用方法M-L2-02
- 具体操作:删除 `internal/ckwk/types.go``type QAList struct{}`
- 退出信号:`go build ./internal/ckwk/...` 通过
- 验证责任AI 自证
- 回滚git revert
### 步骤 2修正 typo "以达到" → "已达到"#9
- 引用方法M-L2-03
- 具体操作:`internal/ckwk/api.go` 两处 "以达到" 改为 "已达到"(约第 168 行和第 416 行)
- 退出信号:`go build ./...` 通过
- 验证责任AI 自证
- 回滚git revert
### 步骤 3确认 schedule.go 时区意图并修正(#8
- 引用方法M-L2-03
- 具体操作:注释写"每天 6 点执行",改为 `Asia/Shanghai` + `0 6 * * *` 对齐注释意图
- 退出信号:`go build ./...` 通过
- 验证责任AI 自证(确认编译通过)+ HUMAN确认业务意图
- 回滚git revert
### 步骤 4提取 userKey 构造为 WK.UserKey() 方法(#4
- 引用方法M-L2-03
- 具体操作:
1.`internal/ckwk/api.go``WK` struct 上添加 `func (wk *WK) UserKey() string { return wk.Host + ":" + wk.Username }`
2.`session_manager.go` 中替换 4 处手动拼接为 `wk.UserKey()``item.Instance.UserKey()`
- 退出信号:`go build ./...` 通过 + grep 确认无残留的 `Host + ":" + Username` 拼接
- 验证责任AI 自证
- 回滚git revert
### 步骤 5提取 getWKFromContext 辅助函数(#1
- 引用方法M-L2-01
- 具体操作:
1.`internal/handler/ckwk.go` 添加 `func getWKFromContext(ctx *gin.Context) (*ckwk.WK, bool)`
2. 替换 5 处重复代码为调用此函数
- 退出信号:`go build ./...` 通过 + grep 确认无残留的 `ctx.Get("wk_instance")` 模式
- 验证责任AI 自证
- 回滚git revert
### 步骤 6提取 retryCode 函数(#2
- 引用方法M-L2-01
- 具体操作:
1.`internal/ckwk/api.go` 添加 `func retryCode(wk *WK, maxRetries int) (string, error)`
2. 替换 `Login()``performStudy()` 中的重复重试代码为调用
- 退出信号:`go build ./...` 通过
- 验证责任AI 自证
- 回滚git revert
### 步骤 7提取 removeSession 内部方法(#3
- 引用方法M-L2-01
- 具体操作:
1.`SessionManager` 上添加 `func (m *SessionManager) removeSession(sessionID string, item SessionItem)`
2. 替换 `Del()``ClearAll()``ClearExpired()` 中的重复清理代码
- 退出信号:`go build ./...` 通过
- 验证责任AI 自证
- 回滚git revert
### 步骤 8SessionManager.Get() 优化锁策略(#5
- 引用方法M-L4-01
- 具体操作:
1. `Get()` 改为先 RLock 读取,只更新 LastValue 时升级为写锁
2. 由于 Go 的 RWMutex 不支持锁升级改为RLock 读 → 检查存在 → RUnlock → Lock 更新 LastValue → Unlock
- 退出信号:`go build ./...` 通过
- 验证责任AI 自证
- 回滚git revert
### 步骤 9GetRecords 递归改迭代(#6
- 引用方法M-L2-01
- 具体操作:将 `GetRecords` 中的递归分页改为 for 循环,逐页追加到 result.List
- 退出信号:`go build ./...` 通过
- 验证责任AI 自证
- 回滚git revert
### 步骤 10prepareRequestClient 缓存配置(#7
- 引用方法M-L4-01
- 具体操作:
1.`WK` struct 上缓存上次配置的 debug/proxy/ssl-verify 状态
2. `prepareRequestClient()` 只在配置变化时重建,否则跳过
3.`conf.SetRuntimeDebugEnabled` 被调用后,下次请求时自动触发重建
- 退出信号:`go build ./...` 通过
- 验证责任AI 自证 + HUMAN切换 debug 开关确认代理/SSL 行为生效)
- 回滚git revert
### 步骤 11修复 bufferHub.append 切片内存泄漏(#12
- 引用方法M-L4-01
- 具体操作:在 `h.entries = append(h.entries[1:], entry)` 之前,将 `h.entries[0]` 置为零值 `Entry{}`
- 退出信号:`go build ./...` 通过
- 验证责任AI 自证
- 回滚git revert
## 4. 风险与看点
- 步骤 8锁优化Go RWMutex 不支持锁升级,需 RLock→RUnlock→Lock 两步走,中间可能有竞态导致 LastValue 偶尔不准但不影响正确性LastValue 只用于过期清理的时间戳判断)
- 步骤 10配置缓存需确保 debug 开关切换后首次请求即生效,不能有"延迟一拍"问题

View File

@@ -0,0 +1,164 @@
---
doc_type: refactor-scan
refactor: 2026-04-25-backend-cleanup
status: pending-user-selection
scope: 后端 Go 代码全部internal/、pkg/、cmd/21 文件 ~2485 行
summary: 发现 12 条优化点:结构 4 / 性能 3 / 可读性 5按风险低 7 / 中 4 / 高 1
---
# backend-cleanup scan
## 总览
- 扫描范围:后端 Go 代码全部internal/ckwk、internal/conf、internal/dto、internal/handler、internal/middleware、internal/router、internal/schedule、pkg/common、pkg/log、pkg/request、cmd/21 文件 ~2485 行
- 发现 12 条优化点:结构 4 / 性能 3 / 可读性 5
- 按风险:低 7 / 中 4 / 高 1
- 建议先做:#1 #2 #3 #5 #6 #7 #10低风险、纯提取、AI 可用 go vet 自证)
- 建议慎做 / 后做:#8(改并发原语需测试)、#11(跨模块分层需 architecture 同步)
- 前置检查 7 条:第 2 条命中(测试覆盖率为 0但本次条目多为纯机械提取不改变控制流风险可控第 8 条改锁策略为中风险,建议补测试后再做
## 条目
### #1 提取 handler 中 wk_instance 获取的重复代码 ✓
- **位置**`internal/handler/ckwk.go:68-74, 98-103, 124-129, 143-148, 170-175`
- **分类**:结构
- **现状**5 个 handler 方法开头都有相同的 `ctx.Get("wk_instance")` + 类型断言 + 错误响应,每处 4-5 行
- **问题**:同一段代码重复 5 次25 行→5 行),任何一处错误响应格式变了要改 5 处
- **建议**:提取 `getWKFromContext(ctx *gin.Context) (*ckwk.WK, bool)` 函数,返回 `(wk, ok)`,调用方统一处理
- **建议映射的方法**M-L2-01提取函数
- **风险**:低(纯提取,不改变控制流)
- **验证**AI 自证go vet + go build
- **范围**:约 20 行 / 1 文件
### #2 提取验证码重试逻辑为独立函数 ✓
- **位置**`internal/ckwk/api.go:159-169, 407-417`
- **分类**:结构
- **现状**`Login()``performStudy()` 各有一段完全相同的 3 次验证码获取重试循环,含相同的错误日志和"以达到最大重试次数"提示
- **问题**:同一段逻辑重复 2 次14 行×2且重试次数硬编码在两处typo "以达到" 在两处都存在
- **建议**:提取 `retryCode(wk *WK, maxRetries int) (string, error)` 函数Login 和 performStudy 调用它
- **建议映射的方法**M-L2-01提取函数
- **风险**:低(纯提取,控制流等价)
- **验证**AI 自证go vet + go build
- **范围**:约 14 行 / 1 文件
### #3 提取 SessionManager 的 removeSession 内部方法 ✓
- **位置**`internal/ckwk/session_manager.go:97-108, 143-155, 165-177`
- **分类**:结构
- **现状**`Del()``ClearAll()``ClearExpired()` 三个方法共享相同逻辑cancel context → 构建 userKey → 删两个 map → 记日志
- **问题**:同一段逻辑重复 3 次(约 8 行×3任一处 cleanup 逻辑变了要改 3 处
- **建议**:提取 `removeSession(sessionID string, item SessionItem)` 内部方法,三个调用方统一使用
- **建议映射的方法**M-L2-01提取函数
- **风险**:低(纯提取,不改变控制流)
- **验证**AI 自证go vet + go build
- **范围**:约 16 行 / 1 文件
### #4 提取 userKey 构造为 WK 方法 ✓
- **位置**`internal/ckwk/session_manager.go:37, 98, 149, 171`
- **分类**:可读性
- **现状**`wk.Host + ":" + wk.Username``item.Instance.Host + ":" + item.Instance.Username` 在 4 处手动拼接
- **问题**:相同的 key 构造逻辑散布 4 处,如果 key 格式变了要改 4 处;且语义不明确(读代码时不确定 key 是什么格式)
- **建议**:在 WK 上添加 `UserKey() string` 方法返回 `wk.Host + ":" + wk.Username`SessionManager 内部统一调用
- **建议映射的方法**M-L2-03提取变量/查询)
- **风险**:低(纯提取,字符串拼接等价)
- **验证**AI 自证go vet + go build
- **范围**:约 4 行 / 2 文件session_manager.go + api.go
### #5 SessionManager.Get() 用写锁做读操作 ✓
- **位置**`internal/ckwk/session_manager.go:80-91`
- **分类**:性能
- **现状**`Get()` 使用 `m.mu.Lock()`(写锁),但主要操作是读取 session只更新了 `LastValue` 字段
- **问题**写锁使所有读操作串行化在并发请求下成为瓶颈KeepAlive goroutine 每 2 分钟读一次 + 高频 API 调用读一次,全部互斥
- **建议**:改为 RLock 读 + CAS 更新 LastValue或用 RLock 检查存在性后只在需要更新时加写锁
- **建议映射的方法**M-L4-01优化锁粒度
- **风险**锁策略改变但语义等价LastValue 更新丢失不影响正确性,只是保活时间戳偶尔不准)
- **验证**AI 自证go vet + go build + 并发场景手动确认无死锁)
- **范围**:约 8 行 / 1 文件
### #6 GetRecords 递归分页改为迭代 ✓
- **位置**`internal/ckwk/resp.go:79-92`
- **分类**:性能
- **现状**`GetRecords` 在分页获取时递归调用自身 `GetRecords[T](wk, rType, courseID, nextPage)`
- **问题**:如果课程有大量分页(>100 页递归深度会很大Go 没有尾调用优化,每层递归占用栈帧;极端情况栈溢出
- **建议**:改为 for 循环迭代,逐页追加到 result.List
- **建议映射的方法**M-L2-01提取函数改为迭代实现
- **风险**:低(行为等价,输入输出不变)
- **验证**AI 自证go vet + go build
- **范围**:约 15 行 / 1 文件
### #7 prepareRequestClient 不应每次请求都重建配置 ✓
- **位置**`internal/ckwk/api.go:89-113`
- **分类**:性能
- **现状**:每次调用 `newRequest()` 都调用 `prepareRequestClient()`,重建 Config 对象、解析代理 URL、创建 Transport、设置 Cookie
- **问题**:每次 HTTP 请求都重建配置是冗余的debug 设置在运行期间极少变化Transport 创建涉及 TLS 配置,开销不低
- **建议**:只在 debug 配置变更时重建配置(可加一个 dirty flag 或在 UpdateDebugConfig 时触发),或缓存 Config 只在参数变化时重建
- **建议映射的方法**M-L4-01缓存/延迟计算)
- **风险**:中(改变了"何时重建配置"的语义,需要确保 debug 开关切换时能立即生效)
- **验证**AI 自证go vet + go build+ HUMAN切换 debug 开关后确认代理/SSL 行为生效)
- **范围**:约 20 行 / 1 文件
### #8 schedule.go 时区和 cron 表达式不一致 ✓
- **位置**`internal/schedule/schedule.go:14, 21`
- **分类**:可读性
- **现状**:时区设为 `Asia/Singapore`,但 cron 表达式是 `0 2 * * *`(凌晨 2 点新加坡时间),注释写"每天 6 点执行"
- **问题**:注释和代码不一致——如果意图是北京时间 6 点,时区应为 `Asia/Shanghai`;如果意图是新加坡时间凌晨 2 点清理,注释错误
- **建议**:根据业务意图统一:改为 `Asia/Shanghai` + `0 6 * * *`(北京时间 6 点),或改为 `Asia/Singapore` + 更新注释
- **建议映射的方法**M-L2-03修正不一致
- **风险**:低(修注释+时区或修 cron 表达式,行为由用户确认意图后决定)
- **验证**HUMAN确认业务意图是北京时间 6 点还是其他?)
- **范围**:约 2 行 / 1 文件
### #9 错误信息 typo "以达到" → "已达到" ✓
- **位置**`internal/ckwk/api.go:168, 416`
- **分类**:可读性
- **现状**:两处错误信息写"以达到最大重试次数",应为"已达到""已经达到"的缩写)
- **问题**:错别字,影响用户看到的错误提示
- **建议**:改为"已达到最大重试次数"
- **建议映射的方法**M-L2-03修正
- **风险**:低(只改字符串字面量)
- **验证**AI 自证go build
- **范围**:约 2 行 / 1 文件
### #10 删除未使用的 QAList struct ✓
- **位置**`internal/ckwk/types.go:52`
- **分类**:可读性
- **现状**`type QAList struct{}` 声明但从未使用
- **问题**:死代码,增加阅读负担,可能误导后续开发者以为有 QA 功能
- **建议**:删除
- **建议映射的方法**M-L2-02内联/删除空壳)
- **风险**:低(无引用)
- **验证**AI 自证go build 无编译错误)
- **范围**:约 3 行 / 1 文件
### #11 Handler 层业务逻辑下沉到 Service 层 ✗
- **位置**`internal/handler/ckwk.go` 全文
- **分类**:结构
- **现状**`Login()` handler 包含 Cookie 构造、WK 实例创建、登录流程编排、Session 存储;`AllRecord()` handler 包含分页归一化、按类型分发、响应组装
- **问题**Handler 承担了业务逻辑Login 45 行、AllRecord 60 行),违反分层原则,难以对业务逻辑做单元测试
- **建议**:提取 Service 层Handler 只做参数绑定→调用 Service→组装响应
- **建议映射的方法**M-L3-04服务层抽取
- **风险**:高(跨多文件、改公开接口、需要 Parallel Change
- **验证**HUMAN功能验证
- **范围**:约 100+ 行 / 3+ 文件
### #12 bufferHub.append 切片内存泄漏 ✓
- **位置**`pkg/log/buffer.go:59`
- **分类**:性能
- **现状**`h.entries = append(h.entries[1:], entry)` 在超过 limit 时用 re-slice 淘汰旧条目,但底层数组仍持有对被淘汰 Entry 的引用,阻止 GC 回收
- **问题**:环形缓冲区每淘汰一条,底层 array 就泄漏一个 Entry 大小的内存,长期运行后会缓慢增长
- **建议**:淘汰时将被移除的位置显式置 nil`h.entries[0] = Entry{}` 后再 re-slice或改用环形索引实现避免 re-slice
- **建议映射的方法**M-L4-01修复内存泄漏
- **风险**:低(行为等价,只影响 GC 行为)
- **验证**AI 自证go vet + go build
- **范围**:约 3 行 / 1 文件