Commit Graph
11 Commits
Author SHA1 Message Date
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 e807dea218 feat(share-card): 字体自动发现 + 启动探测,缺字体不再等到第一次请求才 500
- 系统候选补上 glyf 轮廓的 CJK 字体(文泉驿正黑/微米黑、DroidSansFallback、
  AR PL UMing),并把 Noto Sans CJK 压到最后:它是 CFF/OTTO,freetype 必定解析失败,
  排前面只会白白多读十几 MB
- static/fonts 同时按当前工作目录与可执行文件目录查找,systemd 没配
  WorkingDirectory 时也能找到(原先只按 CWD,文件在却报「没有可用字体」)
- 启动时探一次字体:命中打 Info 带路径,缺失打 WARN 并给出补救命令
  (apt install fonts-wqy-zenhei / 放 static/fonts/ / 配 font_path)
- .ttc 先提取首个字面再用(仅对 glyf 轮廓的集合有效,如文泉驿);
  失败结果缓存 1 分钟,避免公开端点被「每请求重新解析十几 MB」放大

实测:本地实例 apt install fonts-wqy-zenhei 后无需任何配置,启动即打出
font=/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc,/share-card 与单题分享图
从 500 变为 200 image/jpeg,中文与二维码渲染正常。
2026-10-27 12:37:48 +08:00
tamakyi 675cecb21d fix(share): 单题卡缓存 key 补 user.Domain;纠正过期的 TTL 注释
先查了「缓存 key 漏项」到底有没有实际影响,结论是**当前没有**:

- 用户域名只在注册时写入(register.go / CreateUser),没有改域名的入口,
  所以两张卡的二维码内容不会在运行期变化
- ExternalURL / ShareBackground 来自配置,改了必须重启,而缓存是进程内的,
  重启即清空 —— 不存在「配置变了缓存还在」的窗口
- 头像、背景上传走 R2 随机 key(picture/日期/随机 hex,内容不可变),
  换图必然换 URL,key 随之变化,不会出现「同名不同图」

只有一条理论缺口:域名要是将来能被修改,单题卡 key 没算 user.Domain,
会出现最长一小时的「二维码指向旧域名」。补上这一项成本为零,先按事实算进去。
shareCardCacheKey 本来就含 Domain,两张卡现在一致。

顺带把两处「TTL 30 分钟」的注释改成 1 小时(代码一直是 time.Hour)。
新增 TestShareCardCacheKeysCoverDrawnFields:逐个改动画进图里的字段,
断言 key 必须随之变化;去掉 user.Domain 后立刻报
"user.Domain is drawn on the question card but not in questionShareCardCacheKey"。
2026-10-23 00:58:31 +08:00
tamakyi 310f85156a perf(share): 单题卡二维码同样提到 5px/模块
单题卡右侧卡片只有 210×255,二维码最大只能到 148px(37 模块 = 4px/模块),
缩到 480 宽照样解不出。

- QQRCardW 210 → 236、QQRCardH 255 → 300、QQRSize 150 → 190(37 模块 → 185px)
  卡片仍居中对齐(x 679、y 120),左侧文字区随 QQRCardX 派生,自动收窄 26px
- 二维码卡片绘制抽成 drawQuestionQRCard,可在测试里直接调用出图
- 新增 TestQuestionQRCardLayout:二维码必须正方形、模块 ≥5px、四边留白 ≥20px、
  居中偏差 ≤1px,TEST_PREVIEW=1 输出整卡 PNG;把 QQRSize 改回 150 立刻 FAIL

实测:二维码 185px(5px/模块)、居中偏差 0.5px、上下留白 34px;
gozxing 在 960 / 720 / 480 / 320 宽下都能解出正确链接。
2026-10-23 00:27:07 +08:00
tamakyi 72f08cd7c9 perf(share): 用「同画布放大二维码」替代 2x 渲染,模块 4px → 5px
960×540 的卡被聊天软件缩到 480 宽时,二维码每模块只剩 2px,解码器读不出。
2x 渲染能解决,但实测成本不低(见下),先用零代价的办法把模块做大。

实测矩阵(go-qrcode 纠错 Q / 真实 55 字符链接 / 37 模块,含内嵌头像):
  尺寸 148px = 4.0px/模块 -> 960 o 720 o 480 x 320 x
  尺寸 190px = 5.0px/模块 -> 960 o 720 o 480 o 320 o 240 x
纠错降到 M(33 模块)只省 11% 模块数,容错却从 25% 掉到 15%,不划算;
PNG 只能保住原图,缩到 480 一样解不出,体积还是 JPEG 的 5 倍。

- QRCodeSize 165 → 190(37 模块取整为 5px/模块;生产短域名 33 模块为 165px)
- QRCardH 315 → 335 容纳变高的二维码,卡片整体仍垂直居中
- 新增 TestGenerateQRModuleSizeAtLeast5:三种常见链接长度下模块必须 ≥5px,
  把 QRCodeSize 改回 165 立刻 FAIL

渲染成本基准(1x vs 2x):JPEG 编码 12 → 48ms、背景 Lanczos 缩放 25 → 41ms、
画布分配 3 → 13ms,二维码生成本身 0.8ms —— 2x 的代价主要在编码与缩放,
与二维码尺寸无关,所以放大二维码不增加任何 CPU。
2026-10-22 23:36:02 +08:00
tamakyi b35c2a8f45 fix(share): 二维码不再被静默区吃掉三分之一尺寸;卡内间距收到 4px 栅格
上一版改成按模块整数倍出图时,静默区仍然画在图像里:49 个模块(41 + 4×2)
挤进 165px 只剩 3px/模块、符号 111px,比改造前的 135px 还小 —— 这是自己引入的
回归。测试卡上验证时才发现:二维码在 960 宽的卡里只占 111px,微信/QQ 二次
压缩到 480 宽后模块不足 1.5px,gozxing 直接解不出来。

- generateQR 改 qr.DisableBorder:图像只输出符号,静默区交给承载它的白色卡片
  (四周留白 30px 以上,远超 4 模块)。符号尺寸按 size/modules 取整,最多
  浪费一个模块宽度
- 调用方按实际符号尺寸居中与排版,不再假设等于目标尺寸
- 单题卡 QQRSize 128 → 150:37 模块的题目链接因此拿到 4px/模块(原 3px)
- 卡内垂直间距 26/21/21/15 → 24/20/20/16,落在 4px 栅格上

验证:profile 卡二维码 111px → 148px(37 模块 × 4px,用满 90%),单题卡
111px → 148px;gozxing 解码 960 原图、75% 缩放图均能读出正确 URL(含内嵌
头像)。守门测试加了两条:符号宽度必须等于 modules×perModule 且吃掉目标尺寸
≥80%(变异成「含静默区」立刻 FAIL),头像测试改判四角必须是定位图案而非
白色(防止符号被裁切)。
2026-10-22 23:22:34 +08:00
tamakyi d51629ef2f fix(share): 字体缺失不再永久失败;二维码按模块整数倍绘制
测试环境部署后分享卡整站 500「字体文件未配置或无法加载」。根因不是代码
回退,而是字体从来就不在仓库里:static/fonts/ 既没有入库(git 历史为空),
也没有 embed,凡是按仓库新建的环境都缺这个文件,只有手工补过字体的老环境
才正常。

- getFont 不再用 sync.Once 永久缓存失败:补上字体文件即生效,无需重启;
  失败时错误信息带上「已尝试的路径」,排障不用再猜是配置还是文件
- 候选路径:font_path → static/fonts/*.{otf,ttf,ttc} → 常见系统 CJK 字体,
  换过字体文件名也能用
- app.sample.ini 注明字体必须自备、缺失的后果

顺带做 P1 的第一项:generateQR 改为按模块整数倍出图(41 模块 @165px →
每模块 4px、符号 164px 居中)。原先是 256px 生成后最近邻缩到 165px,
每模块 4.02px,模块宽窄相差 1px,小尺寸二次压缩后更容易扫不动。
内嵌头像改为按目标尺寸等比(size*48/256)+ 2px 白边,静默区不受影响。

守门测试新增两条:TestGenerateQRIsModuleAligned(扫描线游程必须是模块
宽度的整数倍,变异成等比缩放后立刻 FAIL)、TestGenerateQRWithLogo
(中心是头像、四角静默区纯白)。
2026-10-22 22:33:22 +08:00
tamakyi 0054e508b7 fix(share): 分享卡链接按真实字宽居中,长链接按 host/path 收敛
原实现用字符宽度系数估算文本宽度,把标点/符号一律按 1em 计,
而 URL 里的 : / . _ - 实际只有 0.25~0.35em,整句高估 55%:
15px 的 box.shiroko.one/_/tamakyi 估算 352.5px、真实 227.9px,
越过卡片可用宽 230px 后触发等比缩字号,最终只画到 9.6px 且左偏 40px;
链接越长字号越小(长域名实测 5.3px),整段挤在卡片左侧。

- measureTextWidth 改为逐字累加 HMetric.AdvanceWidth + Kern,
  scale = fontSize*64 与 freetype 在 72 DPI 下的换算一致,测量结果与
  DrawString 返回笔位逐位相等;字体不可用时回退估算
- drawTextCentered 随之修正,底部水印原本偏 9.8px 一并归位
- 新增 displayURL(去协议前缀)/ layoutLink / ellipsizeMiddle:
  单行 15→13px;放不下按 host/path 拆两行 13→11px;host 超宽才中间省略;
  字号下限 11px,行长恒定不超过 QRCardW-40
- 卡片内容高度按链接行数计算,两行时整体重新居中

守门测试见 route/question/share_layout_test.go(测量=笔位、限宽、按着墨
边界判定居中 ±3px、TEST_PREVIEW=1 出图)。
2026-10-22 21:42:50 +08:00
tamakyi 4592b6ef24 fix v13 2026-10-19 01:58:00 +08:00
tamakyi 6bd90779f8 restrict v1 2026-10-18 21:14:59 +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