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"。
This commit is contained in:
@@ -328,7 +328,7 @@ func (c *ttlCache) cleanup() {
|
||||
// 社交图标 URL 固定不变,基本永远命中;头像/背景变更时最多延迟 1 小时生效。
|
||||
var imageCache = newTTLCache(24)
|
||||
|
||||
// 分享图结果缓存,TTL 30 分钟。key 含用户资料指纹,资料变更后自动失效。
|
||||
// 分享图结果缓存,TTL 1 小时。key 含用户资料指纹,资料变更后自动失效。
|
||||
// 存的是 PNG 编码字节(约 0.2~1MB/张),限制 32 条,总内存 ≤ 32MB。
|
||||
var shareCardCache = newTTLCache(32)
|
||||
|
||||
@@ -561,7 +561,9 @@ func writeShareCardResponse(ctx context.Context, data []byte) {
|
||||
}
|
||||
|
||||
// shareCardCacheKey 基于用户资料生成缓存指纹。
|
||||
// 资料(名称/简介/头像/背景/社交链接)任何一项变更都会导致 key 变化,缓存自动失效。
|
||||
// 资料(域名/名称/简介/头像/背景/社交链接)任何一项变更都会导致 key 变化,缓存自动失效。
|
||||
// conf 里的 ExternalURL / ShareBackground 不入 key:改配置必须重启,进程内缓存随之清空。
|
||||
// 头像、背景走 R2 随机 key(内容不可变),换图即换 URL,也不会出现同名不同图。
|
||||
func shareCardCacheKey(user *db.User) string {
|
||||
h := md5.New()
|
||||
fmt.Fprintf(h, "%s|%s|%s|%s|%s|%s|%s|%s|%s|%s",
|
||||
@@ -948,7 +950,7 @@ func drawQuestionQRCard(canvas *image.RGBA, user *db.User, question *db.Question
|
||||
return nil
|
||||
}
|
||||
|
||||
// 单题分享图结果缓存,TTL 30 分钟,上限 16 条(1C1G 内存友好)。
|
||||
// 单题分享图结果缓存,TTL 1 小时,上限 16 条(1C1G 内存友好)。
|
||||
var questionShareCache = newTTLCache(16)
|
||||
|
||||
// QuestionShareCard 生成单个问题(问题+回答)的分享卡片图。
|
||||
@@ -1111,9 +1113,12 @@ func QuestionShareCard(ctx context.Context) {
|
||||
}
|
||||
|
||||
// questionShareCardCacheKey 基于问题内容与用户资料生成缓存指纹,内容/资料变更自动失效。
|
||||
// user.Domain 虽然当前不可改(只在注册时写入),但它进了二维码内容:一旦将来开放改域名,
|
||||
// 漏掉它就会出现「卡片二维码指向旧域名」的一小时窗口,这里先按事实把它算进去。
|
||||
func questionShareCardCacheKey(user *db.User, question *db.Question) string {
|
||||
h := md5.New()
|
||||
fmt.Fprintf(h, "q|%d|%s|%s|%s|%s|%s", question.ID, question.Content, question.Answer, user.Name, user.Avatar, user.Background)
|
||||
fmt.Fprintf(h, "q|%d|%s|%s|%s|%s|%s|%s",
|
||||
question.ID, question.Content, question.Answer, user.Domain, user.Name, user.Avatar, user.Background)
|
||||
return fmt.Sprintf("qshare:%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
|
||||
@@ -349,6 +349,59 @@ func qrModules(t *testing.T, content string) int {
|
||||
return len(q.Bitmap())
|
||||
}
|
||||
|
||||
// 缓存 key 必须覆盖所有画进图里的资料:漏一项就会出现「改了资料,卡片还是一小时旧图」。
|
||||
func TestShareCardCacheKeysCoverDrawnFields(t *testing.T) {
|
||||
base := db.User{Domain: "d", Name: "n", Intro: "i", Avatar: "a", Background: "b",
|
||||
Acfunlink: "1", Bililink: "2", Weibolink: "3", Bloglink: "4", Xiaohongshulink: "5"}
|
||||
baseKey := shareCardCacheKey(&base)
|
||||
|
||||
userMuts := []struct {
|
||||
field string
|
||||
apply func(u *db.User)
|
||||
}{
|
||||
{"domain", func(u *db.User) { u.Domain = "x" }},
|
||||
{"name", func(u *db.User) { u.Name = "x" }},
|
||||
{"intro", func(u *db.User) { u.Intro = "x" }},
|
||||
{"avatar", func(u *db.User) { u.Avatar = "x" }},
|
||||
{"background", func(u *db.User) { u.Background = "x" }},
|
||||
{"acfun", func(u *db.User) { u.Acfunlink = "x" }},
|
||||
{"bili", func(u *db.User) { u.Bililink = "x" }},
|
||||
{"weibo", func(u *db.User) { u.Weibolink = "x" }},
|
||||
{"blog", func(u *db.User) { u.Bloglink = "x" }},
|
||||
{"xiaohongshu", func(u *db.User) { u.Xiaohongshulink = "x" }},
|
||||
}
|
||||
for _, m := range userMuts {
|
||||
u := base
|
||||
m.apply(&u)
|
||||
if shareCardCacheKey(&u) == baseKey {
|
||||
t.Errorf("user.%s is drawn on the card but not in shareCardCacheKey", m.field)
|
||||
}
|
||||
}
|
||||
|
||||
question := db.Question{Content: "c", Answer: "a"}
|
||||
question.ID = 7
|
||||
qKey := questionShareCardCacheKey(&base, &question)
|
||||
questionMuts := []struct {
|
||||
field string
|
||||
apply func(u *db.User, q *db.Question)
|
||||
}{
|
||||
{"question.ID", func(u *db.User, q *db.Question) { q.ID++ }},
|
||||
{"question.Content", func(u *db.User, q *db.Question) { q.Content = "x" }},
|
||||
{"question.Answer", func(u *db.User, q *db.Question) { q.Answer = "x" }},
|
||||
{"user.Domain", func(u *db.User, q *db.Question) { u.Domain = "x" }},
|
||||
{"user.Name", func(u *db.User, q *db.Question) { u.Name = "x" }},
|
||||
{"user.Avatar", func(u *db.User, q *db.Question) { u.Avatar = "x" }},
|
||||
{"user.Background", func(u *db.User, q *db.Question) { u.Background = "x" }},
|
||||
}
|
||||
for _, m := range questionMuts {
|
||||
u, q := base, question
|
||||
m.apply(&u, &q)
|
||||
if questionShareCardCacheKey(&u, &q) == qKey {
|
||||
t.Errorf("%s is drawn on the question card but not in questionShareCardCacheKey", m.field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 视觉自检:TEST_PREVIEW=1 时把链接区渲成 PNG,人工确认断行与对齐。
|
||||
func TestShareCardLinkPreview(t *testing.T) {
|
||||
if os.Getenv("TEST_PREVIEW") == "" {
|
||||
|
||||
Reference in New Issue
Block a user