--- 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 文件