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

7.9 KiB
Raw Blame History

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、图片前端压缩。