tamakyi
0124e392c7
fix(correctness): 三处幂等缺陷、私聊首条事务化、验证码失败不再丢正文
...
四处都属于「并发/失败路径下状态不一致」,用户侧表现为「点了没反应」「发了没收到」。
1) 删除会话重复回退配额
MarkDeletedByOwner 是 check-then-act:双击时两个请求都读到 is_deleted_by_owner=false,
都执行 UPDATE、都执行一次 IncrementQuota(-1)。计数不会变负,但会白吃掉其他会话的额度,
表现为定量收集提前关闭,箱主重置计数也校准不回来。
→ 改条件更新 + RowsAffected 决定是否回退。
2) 双击产生两条重复追问
「上一条追问必须先被回复」原来只在 handler 里判断(事务外):两个请求都能通过那次检查,
随后被 CreateIfBelowLimit 的问题行锁串行化,各自 count 都 < 3 —— 两条相同追问都建成,
吃掉 2/3 名额,箱主还收两封邮件。
→ 判定挪进事务,新增 ErrFollowUpAwaitingReply;handler 的预检查保留作为快速失败。
3) 双击置顶结果与预期相反
SetPinned(id, !question.IsPinned) 是 read-modify-write:两个请求都读到 false 都写 true,
用户点两次期望「置顶后取消」,实际仍是「置顶」。
→ 新增 SetPinnedIfCurrent(current, next),第二次因状态已变而 0 行,语义是「只切换一次」。
4) 私聊首条三步写入无事务
建会话 → 写首条消息 → 计数是三次独立写入,第二步失败会留下 message_count=0 的空会话,
箱主列表里能点进去却什么都没有;而 BeforeCreate 已占掉一个 6 字符 token(唯一索引),
孤儿会话永久占用它。
→ 新增 CreateWithFirstMessage,三步收进一个事务。
5) 验证码失败丢正文
该分支原来走 SetErrorFlash + Redirect,而其它失败分支都是 SetError + Success 回填表单。
验证码失败恰恰是最常见的失败(token 过期、网络抖动),结果用户写好的正文整段丢失。
→ 改为同请求渲染并回填;错误文案不再透传 err.Error()(可能含上游原始信息)。
6) 5 处裸类型断言改带 ok(share.go ×3 / context.go ×1 / user/vote.go ×1),
断言失败时丢弃脏缓存并走重建,而不是 panic 掉整个请求。share.go 的 ttlCache 补 Delete。
验证:内部 DB 测试 3 项(并发删除只回退一次配额 5→4、双击只产生一条追问、
双击只切换一次置顶);verify_round3.py 6/6(验证码失败 200+正文回填+不建提问、
私聊首条 message_count 与实际消息数一致)。
2026-10-29 16:28:34 +08:00
tamakyi
8380fc9e2b
fix(db): 「最后一个回答」不再被置顶/公开切换推到现在
...
`QuestionStats` 的 `last_a` 取的是 `MAX(updated_at)`,而 GORM 的 Update
一律刷新 updated_at:置顶、公开/私密切换、标记已读都会走 Update。
后果是箱主整理一下旧问题(一次回答都没发生),公开接口
`/api/v1/site-stats` 就说「刚刚有人回答了」。
实测(真库,临时测试已删):一条 9 天前回答的提问,置顶/公开切换后
LastAnswerAt 从 `10-15 10:23` 变成 `10-24 10:23`,偏差 216 小时。
改成 `COALESCE(answered_at, updated_at)`:
- 新数据取 answered_at(首次回答时间,改写回答不重置);
- 加列之前的历史行 answered_at 为空,退回 updated_at —— 与改动前口径
一致,不会让这些行从统计里消失(有测试锁住这一半)。
同时删掉 `GetHotQuestions`:全仓零调用点(只有接口声明 + 实现,route/
里 Questions 的 15 个调用不含它),且同样按 updated_at 排序 ——
死代码 + 错误排序,留着就是给以后的人埋坑,要用时按 answered_at 重写。
影响面:`/api/v1/site-stats` 的 `last_answer_at` 字段与 `text` 里的
「最后一个回答发生在」,字段名与结构不变,只是值更准(更早)。无 schema
变更、无接口变更。性能:执行计划仍是 Seq Scan + Aggregate,5 万行实测
7.9ms vs 8.0ms,差异在噪声带内(0~1.8ms,折合每行约 30ns),
而该接口有 30 分钟进程内缓存。
新增两个回归测试:
- TestQuestionStatsLastAnswerIgnoresLaterEdits(改前红、改后绿)
- TestQuestionStatsLastAnswerFallsBackToUpdatedAt(历史行回退)
2026-10-24 10:30:43 +08:00
tamakyi
0226956feb
fix(stats): 回答耗时改用 answered_at,修测试环境实测出的三处问题
...
在测试环境(box-dev-api)上逐项验算后发现「回答耗时」这一口径是错的。
1. 回答耗时不能用 updated_at - created_at(P0)
GORM 的 Update 会自动刷新 updated_at,而已读、置顶、公开/私密切换全走 Update。
实测:一条 10 天前的提问被 SetRead 标记已读后,updated_at 被推到当下(delta=240h)。
于是「回答耗时」实际测的是「提问到最后一次被碰」:线上那 6 条「超过 7 天」
和偏低的 24 小时回复占比都是这么来的。
修法:新增 questions.answered_at(首次回答时间,可空),只在 answer 由空变非空时
写一次,改写回答不重置;聚合改用 COALESCE(answered_at, updated_at),
首次加列时用 updated_at 冻结历史一次(Migrator().HasColumn 判断是否刚加列),
否则历史值会随着后续的已读/置顶继续变大。
2. 环比句式读不通
中文「上月提问量比前一个月 -69.6%」、英文「...: -69.6% questions.」都是病句。
改为「上月提问量环比 %s%%」/「Month-over-month change for last month: %s%%」。
3. 脚注说「当日 0 点的快照」不实
实际是当天第一次访问时算、服务重启也会重算(实测算出时刻 01:11:50,
基准日却写 2026-10-24,说明它含了 0 点之后的数据)。改成准确描述。
4. 数据库故障时请求会排成 N×30s
ovRefresh 串行化 + 30 秒超时意味着数据库挂起时第 N 个请求要等 N×30 秒。
加 30 秒失败冷却:冷却期内直接降级(有旧快照用旧快照),不再碰数据库。
todayNumbers 失败也盖时间戳,不再每个请求都打两条 COUNT。
测试:新增 internal/db/answered_at_test.go(5 例:只记一次、私密路径也记、
分桶以 answered_at 为准、历史行回退、回填),并做过变异验证 —— 把口径改回
updated_at 后测试立即失败。internal/stats 补 3 例冷却测试。全量构建与测试通过
(internal/storage 的构建失败是 overlay 桩的既有问题)。
2026-10-24 01:45:34 +08:00
tamakyi
bd02eb7ed2
fix templates v1
2026-10-22 17:04:10 +08:00
tamakyi
4592b6ef24
fix v13
2026-10-19 01:58:00 +08:00
tamakyi
adcc42cc8c
fix anwser token and database issus
2026-09-15 11:06:09 +08:00
tamakyi
e3e59e7c69
chore: baseline snapshot before moderation-strictness feature
...
Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
2026-09-10 10:56:27 +08:00