tamakyi
0041cc51f8
perf(vote): 结果聚合改 stale-while-revalidate,消除持续投票时的 O(N²)
...
多选 / 评分 / 排序 / 表单的统计必须在 Go 层逐条 json.Unmarshal 全部提交记录
(ListByVoteID 传 nil 不设 LIMIT),而 Submit 每来一票就 InvalidateResultsCache
删掉缓存 —— 于是第 k 票触发一次 O(k) 重算,持续有人投票时稳态是 O(N²)。
一万票的投票在投票期间每次刷新都要扫一万行 JSON。
现在:
- LoadResults 用 GetWithExpiry 读到「已过期但仍在」的旧值,先返回给调用方,
再在后台按投票去重重算(sync.Map 占位 + shared.SafeGo,60s 超时)
- 后台只拷贝 ID/Type/AllowCustom 三个字段,不把 handler 正在用的 *db.Vote 带进 goroutine
- Submit 从 InvalidateResultsCache(删缓存)改为 ExpireResultsCache(标记过期保留旧值),
否则每次提交都会让下一次读退化成同步全量重算
- 选项变更 / 删除投票仍走 InvalidateResultsCache(结构性变化,旧值已无意义)
- TTLCache 新增 GetWithExpiry / Expire:Get 遇到过期项直接删除并返回 false,
「过期」与「不存在」分不开,正是无法做 SWR 的原因
实测(2 万条多选提交):冷启动 0.211s → 过期首读 0.023s(快 9.2x),
后台刷新后 0.009s。缓存单测 3 项覆盖 GetWithExpiry 不删条目 / Expire 保留值。
顺带把 7 处裸类型断言改成带 ok(其中 results.go 的是新代码)。
2026-10-29 16:28:23 +08:00
tamakyi
c971d99bdb
fix(vote): 站方处置覆盖投票链路
...
提问链路上 ActiveLevel 有 7 处调用,投票链路 0 处。于是 L4(整页停用)处置某人后:
/_/domain 已经 302 到 /restricted,但 /vote/{token} 照常渲染、照常收票;被处置者
还能发新投票;探索页的「热门/全部投票」也不做处置过滤 —— 同一张页面里
「箱主消失了、他的投票还在曝光」。这是运营口径不一致,不是越权。
补齐三处,与提问链路对齐:
- vote.Page L4 → 302 /restricted(同箱主页;查询失败 fail-open,与 Pager 一致)
- vote.Submit L4 → 拒绝(页面进不去不代表直接构造 POST 也拦得住)
- VotePublish L2 起拒绝(L2 已是「收窄公开」,不该再新增公开内容)
- 探索页投票列表改用 Restrictions.ActiveLevels 批量过滤(与 GetShowInHome 同口径,
一次查询避免 N+1),功能开关过滤逻辑保持不变
新增字典 key vote.err.restrictedPublish(四语言)。
验证(本地实例):
- L4 后 /vote/{token} 302 → /restricted;撤销后恢复 200
- 探索页该投票出现次数 0(处置中)→ 2(撤销后)
- 草稿投票在 L2 下发布后状态停留 draft,撤销后同一请求变为 published
2026-10-29 15:53:49 +08:00
tamakyi
ced4c1635c
fix(question): 图片上传失败时回滚已创建的提问
...
Questions.Create 在图片处理之前就提交了,失败分支只渲染错误页、不回滚。于是:
箱主收到一条无图提问,提问者只看到「图片过大 / 上传失败」以为没发出去;再想重发
又会被 DuplicateGuard 按「同 IP + 同内容 5 分钟内重复」拦住 —— 既没发成功也重发不了。
现在失败分支先 Questions.DeleteByID 回滚(连带清理追问与图片关联,并回退前面
那次提问配额 +1),再做提示。顺带把不需要的 else 去掉(两个分支都以 return 结束)。
验证:6MB 图提交后提问数增量 0、该行 deleted_at 非空(回滚走的是软删);
无图对照增量 1。两条路径均返回 200,正常流程未受影响。
2026-10-29 15:53:39 +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
7802bb2078
fix(security): 修复安全审计的 P1/P2/P3 项,含全局复查新发现的两处越权与外跳
...
P1:
- 外链图片:markdown 渲染层只放行站内与自有 CDN 图片。配置里的
image_bucket_cdn_host 常写成 host/path,统一归一化后再比对,避免把自家图一起剥掉
- 密码:页面/提问密码最小 6 位;密码比对挪到验证码之后;页面密码失败锁定 5 次/10 分钟
- 客户端 IP:CF-Connecting-IP / EO-Client-IP 只在 TCP 对端可信(回环或 trusted_proxies)
时采信,公网对端带来的权威头一律忽略
P2/P3:
- 跳转参数收敛到 shared.SafeRedirectTarget:拒反斜杠与控制字符(url.Parse 会把 %0d%0a
解成 CRLF,伪造 Location),最后再用 path.Clean 验一次前缀
- 注册不再暴露「邮箱已注册 / 已停用」:与全新注册共用同一个成功出口,只在日志留痕
- CSP:默认 enforce,白名单由配置派生,覆盖 umami(运行时注入)、Turnstile 与
按 sentry_dsn 条件放行的 Sentry CDN
- /user 与 /_ 下发 no-store;补 X-XSS-Protection、Permissions-Policy,并把安全头前移到
静态资源中间件之前(原先静态文件拿不到 nosniff/HSTS)
- 封禁事实落库 ban_records;429 统一带 Retry-After 与本地化文案
- token 比较改常量时间;探索页页码上限;JSON 响应显式 Content-Type
全局复查补的两处:
- 私聊 SendMessage 的 POST 缺 conv.OwnerID 校验,被拉黑者可换 domain 继续发消息
- 页面密码 next 与登录 to 同源的反斜杠外跳
验收:verify_p1_fixes 4/4、verify_round2 19/19、verify_page_next 6/6、
verify_chat_owner 3/3(含变异对照);单测与 go build ./... 全通过。
2026-10-27 12:37:40 +08:00
tamakyi
a5441c99fd
feat(stats): 新增公开数据页 /stats,入口收敛到页脚
...
首页底部那张「站点统计」卡片下线,完整数据移到公开只读页 /stats:
今日实时数、概览六格、近 30 天折线与 12 个月柱状、三个占比环、
7×24 时段热力图、提问字数与回答耗时分布。
- 聚合在 internal/db/analytics.go(SUM(CASE WHEN),方言差异收敛在 analyticsDialectFor)
- 结果按自然日缓存,跨日重算一次后全站共用;重算失败降级返回上一份快照
- 图表零前端依赖:服务端算坐标的 SVG + CSS grid,禁用 JS 页面仍完整
- 环比取「上月 vs 上上月」:本月未过完,否则每月 1 号会得出 -95%
- 入口只在页脚「社区规则」与「找站长」之间;route.Home 不再计算统计
- 补 questions(created_at) 索引;启动后异步预热当天快照
- 测试:analytics 连真库验证软删除不计入、分桶下标、DOW 对齐
2026-10-24 00:57:08 +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
0181fc5c66
fix templates v3
2026-10-22 18:26:30 +08:00
tamakyi
b939d26ff0
fix templates v2
2026-10-22 17:49:25 +08:00
tamakyi
bd02eb7ed2
fix templates v1
2026-10-22 17:04:10 +08:00
tamakyi
38cd41afde
theme v3
2026-10-19 16:46:42 +08:00
tamakyi
3202ef9821
theme v2
2026-10-19 16:16:07 +08:00
tamakyi
e9fba8b9ed
theme v1
2026-10-19 15:23:11 +08:00
tamakyi
12a37e726f
fix v14
2026-10-19 02:25:19 +08:00
tamakyi
4592b6ef24
fix v13
2026-10-19 01:58:00 +08:00
tamakyi
bd613a6f5a
fix v12
2026-10-19 01:11:28 +08:00
tamakyi
7a6417c33d
restrict v8
2026-10-18 23:32:38 +08:00
tamakyi
317c96ca17
restrict v7
2026-10-18 23:04:50 +08:00
tamakyi
bd81398a06
restrict v6
2026-10-18 22:18:21 +08:00
tamakyi
d14d26df17
restrict v5
2026-10-18 21:59:12 +08:00
tamakyi
52f0712eda
restrict v4
2026-10-18 21:50:58 +08:00
tamakyi
efb19606a7
restrict v3
2026-10-18 21:36:03 +08:00
tamakyi
6bd90779f8
restrict v1
2026-10-18 21:14:59 +08:00
tamakyi
da7e76a7db
webp fix v2
2026-10-16 09:39:30 +08:00
tamakyi
e4457e3da4
webp fix v1
2026-10-15 11:41:19 +08:00
tamakyi
5ee2dc1540
fix login error
2026-09-15 13:10:57 +08:00
tamakyi
0e0c16f786
fix deactivate
2026-09-15 12:10:42 +08:00
tamakyi
adcc42cc8c
fix anwser token and database issus
2026-09-15 11:06:09 +08:00
tamakyi
e683e798aa
fix typo error
2026-09-10 19:27:21 +08:00
tamakyi
8e2325eda5
aliyun sensor server
2026-09-10 18:04:19 +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