docs(memory): 第三批修复与三个新踩到的坑

- 2026-10-29.md:第三批 7 项改动与验证数据
- MEMORY.md:三条长期结论 ——
  「用户主动删除 = 真删」(模型全带 DeletedAt,默认 Delete 是软删)、
  「配额必须与内容同事务」(incrementQuotaTx 是唯一实现,且投票口径是票数不是个数)、
  「错误页不套模板」(f.NotFound 不经过组级中间件,注入 template.Data 会 panic)
- topics/quality.md:标注第三批范围

新踩到的三个坑:
1. gofmt 会把注释列表 `// 1) ...` 改写成 `//  1. ...`,被标为未格式化时先排除这个
2. Select(...) 漏列 = 字段静默为空(摘要永远是空串,只有一项测试红)
3. CDP 验证纯前端:chrome 必须作为独立后台进程启动,跟在别的命令后面 `&`
   会随 shell 退出被回收;headless=new 的空标签页 type 常是 other
This commit is contained in:
tamakyi
2026-10-29 17:18:47 +08:00
parent 5def41d0f8
commit 4fcaa197cf
3 changed files with 69 additions and 13 deletions
+51 -13
View File
@@ -91,19 +91,57 @@ gzip 实现质量高、后台移动端短标签方案做得细。
另:本项目 `core.autocrlf=true`,仓库里存 LF、工作区 checkout 成 CRLF,
所以「工作区看到 CRLF」不等于「仓库里是 CRLF」,判断基准要看 `git show`。
### 待办
---
## 第三批(数据安全 + 产品缺口)
1. **删除投票 / 删除提问改成一个事务 + 真删(`Unscoped`)**。
原来四步独立写入、前几步失败只 Warn → 中途失败留下「选项没了票还在」这类
界面修不好的状态,重试时投票已不存在、只能报 not exist。
更关键的是:所有模型都带 `dbutil.Model` 的软删除,原来全是软删 ——
提交记录里的参与者文本、`voter_ip`、`voter_token`、提问里的 `FromIP`
只是「界面上看不见」,库里永久留着。用户点删除的预期是删掉。
2. **配额与内容同事务**:抽出 `incrementQuotaTx(tx, userID, feature, delta)`,
提问 `Questions.Create`、私聊 `CreateWithFirstMessage`、投票提交
`VoteSubmissions.Create`(新增 `QuotaUserID` 字段)都在自己的事务里 +1。
原来 handler 里独立写入且失败只 Warn,`quota_count` 会永久偏低。
3. **追问改回答不再重复发邮件**:`UpdateAnswerByID` 返回 `wasFirst`(事务内读旧值判定),
handler 只在首次回答时发。原来改三遍措辞就发三封。
4. `isQuestioner` 的 token 比较改 `subtle.ConstantTimeCompare`(与 Pager 中间件同口径);
CSV 导出的**表头与选项列**补 `sanitizeCSVCell`(原来只有数据列走它)。
5. 写路径功能开关改 **fail-closed**:新增 `db.FeatureStateOfStrict`,
读配置失败时拒绝而不是回退默认「开启」。
6. **真 404 + HTML 500**:新增 `internal/route/errorpage.go`,`f.NotFound` 不再 302 到首页,
改 `writeErrorPage(ctx, 404, ...)`;`flamego.Recovery()` 换成 `recoveryMiddleware()`。
7. **提问者找回自己的提问**:成功页写 localStorage,箱主页显示提示条,
新增 `POST /api/v1/ask-status`(批量问「哪些已被回答」)。
### 这批踩到 / 学到的
- **flamego 的 `f.NotFound` 拿不到组级中间件**:`template.Templater` 与 `Contexter`
(含 session、CSRF)都挂在 `f.Group("", ...)` 上,NotFound 不经过它们。
所以在 NotFound 里注入 `template.Data` 会直接 panic:
`unable to invoke the 8th handler: value not found for type template.Data`。
错误页最终用**内联 HTML + i18n.T**(`pickErrorPageLang` 从 Accept-Language 猜语言),
不依赖模板系统 —— 错误页本来是最后一道防线,不该依赖可能出错的链路。
- **`gofmt` 会把注释里的列表规范化**:`// 1) ...` 会被改写成 `// 1. ...`。
自己写的中文注释列表如果用了 `1) 2) 3)`,`gofmt -l` 会标为未格式化 —— 不是行尾问题。
- **`Select(...)` 漏列 = 字段静默为空**:`ListByIDsAndUser` 一开始只选了
id/token/answer/created_at,摘要永远是空串,接口 12 项里就这一项红。
- **配额口径要按功能分别确认**:投票的定量收集是「累计收到的票数」(在提交成功时 +1),
不是「创建了几个投票」。所以我一度在删除投票里加了 -1,那是错的 —— 删投票不代表票没发生过。
- **CDP 验证纯前端功能的套路**(`verify_ask_history_ui.mjs`):
`chrome --headless=new --remote-debugging-port=9222`(必须作为独立后台进程跑,
跟在别的命令后面 `&` 会随 shell 退出被回收)→ node 用内置 `WebSocket` 连
`/json/list` 里任一 target → `Runtime.evaluate` 注入 localStorage → 重新 navigate →
断言 DOM → `Page.captureScreenshot`。注意 headless=new 的空标签页 `type` 常是 `other`,
不能只找 `type === "page"`。
- **控制台错误要区分来源**:页面里 R2 图片的 CORS 报错是既有问题,
把它算进「本次改动失败」只会得到假阳性。判据要按 URL/关键字过滤。
### 待办(仍未做)
本批改动已 commit。剩余清单在 `outputs/review-2026-10-29.md` 第四、五节,尚未做的:
- 删除投票的级联 4 步无事务(`route/user/vote.go:1048-1076`)
- 配额递增失败只 Warn → `quota_count` 与真实行数永久漂移(3 处调用点)
- 改写追问回答会重复发「您的提问有回复」邮件
- `isQuestioner` 用裸 `==` 比 token(与中间件口径不一致)
- CSV 导出选项内容未做公式注入转义
- `FeatureStateOf` 查询异常时 fail-open
- AutoMigrate 无版本化(改列名会静默丢数据)
- 无备份脚本(用户图片全在对象存储,只备份 DB 恢复后全是裂图)
- 无备份脚本(图片全在对象存储,只备份 DB 恢复后全是裂图)
- 32 个 `<label>` 无 `for=`、语义标签对比度 1.9:1
- 真 404 页(现在全 302 到首页)、500 纯文本页
产品向(`review-2026-10-29.md` 第六节,按 ROI):提问者用 localStorage 记住自己的提问、
箱主数据面板(全站无 page view 数据)、Telegram/Webhook 通知、JSON-LD + RSS、图片前端压缩。
- 箱主数据面板(全站无 page view 数据)、Telegram/Webhook 通知、JSON-LD + RSS、图片前端压缩