# TamaBox-v4 评审修复落地报告(2026-08-27) 基于 `outputs/TamaBox-v4-code-review-2026-08-27.md` 的 P2/P3 findings,落实 4 项修复。 改动遵循「最小、可读、可回滚」原则;每项均给出根因、改法、验证状态。 --- ## 已落地(4 项) ### P3-1 autoban 404 探测失效 ✅ **文件**:`internal/route/autoban.go` **根因**:`autobanMiddleware` 内 `defer` 在该 handler **返回时**触发,而 Flamego `run()` 会在 handler 返回后自动推进链路,因此 `defer` 执行时下游路由尚未运行,`rw.Status()` 恒为 `0` → `KindNotFound` 分支永远进不去。敏感路径封禁因 `kind` 在 body 内同步置位,不受影响。 **修复**:在 `defer` 之后、函数返回前加 `ctx.Next()`。经核对 `flamego@v1.9.4`:`Next()` 即 `c.index++; c.run()`,会把下游全部跑完并让 `c.index` 越过末尾,外层循环随后只「超调」一次即退出,**不会重复执行下游**。 **验证**:`internal/security/autoban` 单测通过(安全核心逻辑);`gofmt` 语法校验通过。 ### P3-4 草稿/归档提交提示误导 ✅ **文件**:`route/vote/vote.go`(`Submit`,约 647 行) **根因**:`!isVoteActive` 仅二分「已结束 / 尚未开始」。草稿(`draft`)与归档(`archived`)状态均 `Status != published` 且通常无 `EndAt`,双双落入「尚未开始」,而重定向目标 `/vote/{LinkToken}` 本身会拦截草稿/归档 → 死循环式跳转。 **修复**:按 `vote.Status` 精确分支: - `draft` → 「该投票尚未发布」 - `archived` → 「该投票已归档,无法参与」 - 其余:`isVoteEnded` → 「该投票已结束」/ 否则「该投票尚未开始」 **验证**:`gofmt` 语法校验通过;状态常量 `db.VoteStatusDraft/Archived` 已核实存在。 ### P2-2 匿名投票强制验证码 ✅ **方向**(用户拍板):匿名投票始终要求验证码(或行为验证码)。 **文件**:`route/vote/vote.go`(`Submit`)、`route/user/vote.go`(`parseVoteMeta` / `renderVoteFormWithInput`)、`templates/user/vote-form.html` **根因**:原逻辑 `if vote.RequireCaptcha` 仅在创建者开启验证码时才校验匿名提交;关闭后匿名用户清除 `vote_fp` Cookie 即可换签名 token 重复投票,仅剩 `limit*10` 的 IP 兜底。 **修复**: 1. `Submit` 匿名分支门槛由 `if vote.RequireCaptcha` 改为 `if conf.Recaptcha.CaptchaType != ""`——**只要全局配置了验证码,匿名提交一律校验**;未配置(无密钥部署)时不阻断,避免整崩。 2. `parseVoteMeta`(草稿创建/编辑、发布中编辑三路共用的唯一 `require_captcha` 读取点)与 `renderVoteFormWithInput` 强制 `requireCaptcha := true`,使存储值与策略一致。 3. 模板复选框置为 `checked disabled`,对 owner 明确「不可关闭」。 **对齐既有决策**:`db.Votes.RequireCaptcha` gorm 默认 `true`,`votes_test.go:63` 断言「匿名验证码默认开启(决策 #3)」,i18n 即「匿名投票需通过验证码」。本修复使该决策不可被 UI 关闭。 **验证**:`gofmt` 语法校验通过;`conf.Recaptcha.CaptchaType` 字段在 `internal/context/context.go:269` 已使用,类型无误。 ### P2-1 结果缓存写穿失效 ✅(务实版) **文件**:`internal/cache/cache.go`、`route/vote/vote.go`(`Submit`) **根因**:`LoadResults` 对 `multiple/rating/rank/form` 类型需 `ListByVoteID` 全量加载并在 Go 层逐条 `json.Unmarshal` 聚合(O(N));`resultsCache`(30s TTL)仅在过期时重算,新提交后结果页最多陈旧 30s,且每次窗口到期都要重算一次全量。 **修复**: 1. 给 `TTLCache` 新增 `Delete(key)` 方法。 2. `Submit` 成功 `Create` 提交后调用 `resultsCache.Delete(strconv.FormatUint(vote.ID,10))`——**写穿式失效**:新投票后立即清除该投票结果缓存,结果页随即反映,且把 O(N) 重算成本收敛到「每次新投票一次」。 **未做(需独立迁移)**:真正的 O(1) SQL 下推受限于选项选择结果存于 JSON `FormData` 大字段(跨 MySQL/PG 做 JSON 聚合既脆弱又依赖具体版本)。彻底消除 O(N) 需把选择项正态化到独立表(如 `vote_submission_selections(vote_id, submission_id, option_id)`),以 `GROUP BY option_id` 聚合。建议作为后续独立 schema 迁移评估,**不在本次改动内**。 **验证**:`internal/cache` 单测通过(含新 `Delete`);`gofmt` 语法校验通过。 --- ## 本次未改(留作独立决策) - **P3-3 死配置 `MaxVotesPerFingerprint`**:全链路写死为 1、提交侧从不读取,属历史遗留。鉴于设计本意即「一人一票」,建议整体移除该字段与表单接线;涉及 DB schema 迁移,需你确认后再做。 - **P3-2 进程内缓存跨实例陈旧**:`TTLCache` 为进程本地,多实例下清缓存不跨实例。可接受最终一致 / 缩短 TTL / 迁 Redis——架构决策,不急。 --- ## 构建与验证状态 - ✅ 语法:`gofmt -e -l` 对 4 个改动的 .go 文件均无报错。 - ✅ 单元:`go test ./internal/security/autoban/... ./internal/cache/...` 全绿。 - ⚠️ 全量构建受阻(**既有环境问题,非本次改动**):`github.com/chai2010/webp` 依赖 libwebp 本机 C 库(CGO),当前沙箱缺该库,报 `undefined: webpGetInfo` 等。该依赖经传递链被 `route/vote`、`route/user`、`internal/route` 引入,故这几个包在本环境无法完整 `go build`。在带 libwebp 的 CI/Docker 环境中可正常构建。建议你在本地/CI 跑一次全量 `go build ./...` 与 `go test ./...` 做最终确认。 ## 改动文件清单 - `internal/route/autoban.go` - `internal/cache/cache.go` - `route/vote/vote.go` - `route/user/vote.go` - `templates/user/vote-form.html`