From 4fcaa197cf18d1be92ec93a735b413e974fbe2e0 Mon Sep 17 00:00:00 2001 From: tamakyi Date: Thu, 29 Oct 2026 17:18:47 +0800 Subject: [PATCH] =?UTF-8?q?docs(memory):=20=E7=AC=AC=E4=B8=89=E6=89=B9?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8E=E4=B8=89=E4=B8=AA=E6=96=B0=E8=B8=A9?= =?UTF-8?q?=E5=88=B0=E7=9A=84=E5=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .workbuddy/memory/2026-10-29.md | 64 +++++++++++++++++++++++------ .workbuddy/memory/MEMORY.md | 15 +++++++ .workbuddy/memory/topics/quality.md | 3 ++ 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/.workbuddy/memory/2026-10-29.md b/.workbuddy/memory/2026-10-29.md index 482b64e..172c60f 100644 --- a/.workbuddy/memory/2026-10-29.md +++ b/.workbuddy/memory/2026-10-29.md @@ -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 个 `