Files
tamabox/.workbuddy/memory/2026-10-29.md
T
tamakyi 89e263cbd4 docs(memory): 记录第二批修复与「Python write_text 会改行尾」这个坑
- 2026-10-29.md:第二批 9 项改动与验证数据(含投票结果 2 万条 0.211s→0.023s),
  以及剩余待办清单(级联删除事务、配额漂移、JSON-LD/RSS、箱主数据面板等)
- MEMORY.md:构建测试段补 write_text 行尾陷阱与排查手法(看 git diff --stat / git show),
  其它约定段补 /healthz 与 /readyz 的语义边界(存活探针不查依赖)
- topics/quality.md:标注第二批已完成的范围

附带说明:本轮我自己也踩了 write_text 的坑,把 14 个 LF 文件写成了 CRLF,
靠 git diff --stat 的「整文件重写」发现、再用 git show HEAD:<file> 的行尾比对恢复。
2026-10-29 16:29:02 +08:00

110 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-10-29
## 第二轮综合复查(非安全向)
用户要求「再确认还存在什么问题与风险点」+「从用户角度有什么值得改进和增加的功能」。
前一轮是安全专项(P1/P2/P3 全修完),这轮换到**业务正确性 / 运维 / 前端体验 / 产品能力**四个面。
产出:`outputs/review-2026-10-29.md`(分级清单 + 复现方式 + 产品建议)。
### 实测复现的两条 P0
1. **私聊 50 条截断**:`GetByConversationID` 是 `Order("created_at ASC") + Limit(50)`,
唯一调用方永远传 nil cursor;会话上限 200 → 第 51 条起**写库成功但永久不可见**。
本地造 55 条实测:页面只渲染 #1–#50。这不是性能问题,是「数据在库里但用户看不到」。
2. **访问日志泄露 token**:`flamego.Classic()` = `Use(Logger(), Recovery(), Static())`,
Logger 记 `RequestURI`(含 query)。实测日志出现 `path=/_/tblocal/4?t=SECRETTOKEN123`。
`?t=` 是私密题钥匙、`?code=` 是改密凭证。**tracing 中间件早就为同一风险避开了 RequestURI
并写了注释,框架默认 Logger 漏了。**
### 代码确认的 P1
- **无优雅关闭**:全仓 `signal.Notify`/`Shutdown` 零命中,SIGTERM 直接杀进程 → 每次重启掐断在途 POST。
- **提问入库后图片失败不回滚**(`page.go:660` → `:680`),叠加 DuplicateGuard 5 分钟 → 用户以为没发出、5 分钟内不能重提。
- **Redis 两种故障**:session 在 Redis 上宕机即全站 500(flamego session load 直接 panic);
`cache.Cacher` **没有** `if conf.Redis.Addr != ""`,空 Addr 被 go-redis 回退到 localhost:6379 → 没配 Redis 时找回密码静默坏掉。
- **处置不覆盖投票**:提问链路 7 处 `ActiveLevel`,投票链路 0 处。L4 处置后 `/_/domain` 已 302 到 /restricted,
但 `/vote/{token}` 照常可投、可发新投票、探索页照常曝光。
### 产品缺口(重要,别忘)
- **完全没有访问量数据**:`SiteAnalytics` 是全站聚合,无 per-user 维度,也没有 page view 表。
箱主无法知道自己被看了多少次。
- **提问者找不回自己的提问**:不填邮箱时唯一手段是成功页那条链接,关页即失联;全仓 `localStorage` 零命中。
→ 建议前端用 localStorage 记 `{domain, id, token}`,箱主页显示「你有 N 条提问已被回答」。
- `NotifyType` 只有 `email` / `none`,无 Telegram / Webhook。
- 模板零 `application/ld+json`(SEO 富摘要缺失),无 RSS。
- 图片原图直传(无前端压缩),配合 ReadTimeout 30s → 手机端 8MB 图必超时。
### 顺带确认的「不用动」
四语言字典 917 条完全对齐(`TestDictionaryComplete` PASS)、深色模式正文对比度 4.8–6.8:1 达标、
gzip 实现质量高、后台移动端短标签方案做得细。
### 已修(同日,六项全部端到端验证)
| # | 改动 | 验证 |
|---|---|---|
| 1 | `GetByConversationID` 改 `id DESC` 取最新 + 内存反转,limit 取 `conf.Limits.ChatMaxMessages`;游标配 DESC | 55 条会话实测渲染 55 条(改前 50);单测 3 项 + 变异(改回 ASC 变红) |
| 2 | 弃 `flamego.Classic()`,`flamego.New()` + 自写 `accessLogMiddleware`(只记 `URL.Path`;静态成功不记、4xx/5xx 记) | 日志 `msg=access path=/_/tblocal/4`,token 计数 0 |
| 3 | `signal.NotifyContext` + `srv.Shutdown(15s)` + 关连接池 | 8 个并发分享图请求中 SIGTERM → 8/8 成功 |
| 4 | 图片失败分支 `Questions.DeleteByID` 回滚 | 6MB 图 → 提问增量 0 且 `deleted_at` 非空;无图对照增量 1 |
| 5 | `cache.Cacher` 加 `if conf.Redis.Addr != ""`(否则 MemoryIniter);启动 `PingRedis` fail-fast | 停 Redis 后启动立即失败并指向 Redis |
| 6 | 投票 Page/Submit 加 L4、VotePublish 加 L2、探索页用 `ActiveLevels` 批量过滤 | L4 → `/vote` 302 到 /restricted;探索页该投票 0→2(撤销后);L2 下发布停 draft,撤销后 published |
新增 `vote.err.restrictedPublish` 四语言 key。回归:4/4、19/19、6/6、3/3,全量单测 + `go build ./...` 通过。
**踩到的两个坑**(值得记):
- 消息加载的兜底写成 `limit < 50 → 50`,会把箱主显式配置的小上限抬大,测试直接测出这个语义错误。
兜底只该处理**零值**(未初始化),不该替用户改配置。
- Python 的 **bytes 字面量不支持 `\uXXXX`**(只有 str 支持),写字面量 `b'...\uff08'` 会原样写入
`\uff08` 六个字符。CRLF 文件的字节级替换要用 `\xef\xbc\x88` 或 `.encode()`。
### 第二批(P2 + 高价值 P3,Docker 按用户要求跳过)
| # | 改动 | 验证 |
|---|---|---|
| 7 | ReadTimeout 30s→120s(8MB 图需 ≥68KB/s;ReadHeaderTimeout 仍 10s 防 slowloris) | 参数修正 |
| 8 | 投票结果改 stale-while-revalidate:过期先返旧值 + 后台按投票去重重算;Submit 从「删缓存」改「标记过期」(新增 `cache.Expire` / `GetWithExpiry`) | 2 万条:冷启 0.211s → 过期首读 0.023s(9.2x) |
| 9 | 三处幂等:`MarkDeletedByOwner` 条件更新+RowsAffected、`SetPinnedIfCurrent`、追问「上一条已回复」移进事务 | DB 测试 3 项全过 |
| 10 | 私聊首条三写入合并为一个事务(`CreateWithFirstMessage`) | message_count 与实际消息数一致 |
| 11 | `/healthz` + `/readyz`(DB ping 失败 503) | 停 PG 后 healthz 200 / readyz 503 |
| 12 | 验证码失败改同请求渲染,不再丢正文 | 200 + 正文回填 + 不建提问 |
| 13 | `tbBeginSubmit()` 抽出,reCAPTCHA 回调补防重复 | 静态验证(7 个模板受益) |
| 14 | waline 按页加载、`loading="lazy"`、safe-area、三处硬编码接字典(+4 key×4 语言) | /login 无 waline;en 页面零中文残留 |
| 15 | 5 处裸类型断言改带 ok | 编译 + 单测 |
新增 `verify_round3.py`(6 项)。第二批回归:6/6、4/4、19/19、6/6、3/3,全量单测 + build 通过。
**判据随实现更新**:`verify_p1_fixes.py` 的 p1-2b 原来用「是否 302」判断验证码先于密码,
第 12 项改成 200 后判据失效 → 改为「200 且页面报『验证码错误』」。语义不变,判据跟着渲染方式走。
### 这次踩的第三个坑(重要,我自己造成的)
**Windows 上 `Path.write_text()` 默认把 `\n` 转成 `\r\n`**,用它改 LF 文件会让整个文件行尾翻车。
- 症状:`gofmt -l` 突然把整文件标为未格式化。
- 后果:`git diff --stat` 会显示整文件重写。
- 正确写法:`path.write_text(s, encoding="utf-8", newline="\n")`,或用 bytes。
- **排查手段**:`git diff --stat` 是判断行尾污染最快的信号 —— 真实改动应该只有几十行,
整文件重写一定是行尾变了。恢复方式:`git show HEAD:<file>` 取其行尾,再统一替换回来。
另:本项目 `core.autocrlf=true`,仓库里存 LF、工作区 checkout 成 CRLF,
所以「工作区看到 CRLF」不等于「仓库里是 CRLF」,判断基准要看 `git show`。
### 待办
本批改动已 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 恢复后全是裂图)
- 32 个 `<label>` 无 `for=`、语义标签对比度 1.9:1
- 真 404 页(现在全 302 到首页)、500 纯文本页
产品向(`review-2026-10-29.md` 第六节,按 ROI):提问者用 localStorage 记住自己的提问、
箱主数据面板(全站无 page view 数据)、Telegram/Webhook 通知、JSON-LD + RSS、图片前端压缩。