fix templates v2
This commit is contained in:
+127
-31
@@ -7,27 +7,117 @@ package conf
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
// 容器镜像通常不带 tzdata,import 这个包会把一份时区库编进二进制,
|
||||
// LoadLocation("Asia/Shanghai") 才不会因为找不到 zoneinfo 而失败。
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
)
|
||||
|
||||
// defaultAnnouncementZone 裸时间(不带偏移)的默认解释时区。
|
||||
// 站点面向中文用户,运维写「10-25 02:00」时心里想的是北京时间,
|
||||
// 而容器里的 time.Local 是 UTC —— 默认按东八区解释可以避免整体偏移 8 小时。
|
||||
const defaultAnnouncementZone = "Asia/Shanghai"
|
||||
|
||||
// announcementConfig 站内公告([announcement] 段)。
|
||||
//
|
||||
// Content 留空即关闭公告;Start / End 留空表示该侧不限。
|
||||
// 文案留空即关闭公告;Start / End 留空表示该侧不限。
|
||||
// 展示窗口是左闭右开 [Start, End):到 end 那一刻公告消失,
|
||||
// 运维不必为了「准时下线」把时间掐到秒后还担心多挂一分钟。
|
||||
type announcementConfig struct {
|
||||
Content string `ini:"content"`
|
||||
Start string `ini:"start"`
|
||||
End string `ini:"end"`
|
||||
// Content 是默认文案,也是分语言文案缺失时的回退。
|
||||
Content string `ini:"content"`
|
||||
ContentZhCN string `ini:"content_zh_cn"`
|
||||
ContentZhTW string `ini:"content_zh_tw"`
|
||||
ContentEn string `ini:"content_en"`
|
||||
ContentJa string `ini:"content_ja"`
|
||||
|
||||
Start string `ini:"start"`
|
||||
End string `ini:"end"`
|
||||
// Timezone 解释裸时间用的时区名,留空取 defaultAnnouncementZone。
|
||||
Timezone string `ini:"timezone"`
|
||||
|
||||
// 解析后的时间点,零值表示该侧不限。
|
||||
// ini tag 为 "-":既不参与 ini 解析,也不会被未知键检测登记为合法键名。
|
||||
StartAt time.Time `ini:"-"`
|
||||
EndAt time.Time `ini:"-"`
|
||||
StartAt time.Time `ini:"-"`
|
||||
EndAt time.Time `ini:"-"`
|
||||
loc *time.Location `ini:"-"`
|
||||
}
|
||||
|
||||
// 支持的时间格式。裸时间(不带时区偏移)按服务器本地时区解释。
|
||||
// Active 判断给定时刻是否处于公告展示窗口,并返回该语言的正文。
|
||||
// 窗口为左闭右开 [Start, End),两侧留空表示不限。
|
||||
func (a announcementConfig) Active(now time.Time, lang string) (string, bool) {
|
||||
content := a.contentFor(lang)
|
||||
if content == "" || !a.inWindow(now) {
|
||||
return "", false
|
||||
}
|
||||
return content, true
|
||||
}
|
||||
|
||||
// contentFor 按语言取文案;该语言没配就回退到 Content。
|
||||
// Content 也没配则返回空 —— 即「没翻译的语言不显示」,而不是硬塞一门语言。
|
||||
func (a announcementConfig) contentFor(lang string) string {
|
||||
switch i18n.Resolve(lang) {
|
||||
case i18n.LangZhCN:
|
||||
if a.ContentZhCN != "" {
|
||||
return a.ContentZhCN
|
||||
}
|
||||
case i18n.LangZhTW:
|
||||
if a.ContentZhTW != "" {
|
||||
return a.ContentZhTW
|
||||
}
|
||||
case i18n.LangEn:
|
||||
if a.ContentEn != "" {
|
||||
return a.ContentEn
|
||||
}
|
||||
case i18n.LangJa:
|
||||
if a.ContentJa != "" {
|
||||
return a.ContentJa
|
||||
}
|
||||
}
|
||||
return a.Content
|
||||
}
|
||||
|
||||
// inWindow 只判断时间窗,不看语言 —— 启动日志要知道「现在到点了吗」。
|
||||
func (a announcementConfig) inWindow(now time.Time) bool {
|
||||
if !a.StartAt.IsZero() && now.Before(a.StartAt) {
|
||||
return false
|
||||
}
|
||||
if !a.EndAt.IsZero() && !now.Before(a.EndAt) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// hasContent 报告是否配了任何语言的文案。
|
||||
// 只看 Content 会误判「只写了英文文案」为未配置,进而把整条公告关掉。
|
||||
func (a announcementConfig) hasContent() bool {
|
||||
return a.Content != "" || a.ContentZhCN != "" || a.ContentZhTW != "" ||
|
||||
a.ContentEn != "" || a.ContentJa != ""
|
||||
}
|
||||
|
||||
// configuredLanguages 列出真正配了文案的语言,供启动日志核对。
|
||||
func (a announcementConfig) configuredLanguages() []string {
|
||||
var langs []string
|
||||
if a.Content != "" {
|
||||
langs = append(langs, "fallback")
|
||||
}
|
||||
for _, l := range []struct{ code, content string }{
|
||||
{i18n.LangZhCN, a.ContentZhCN},
|
||||
{i18n.LangZhTW, a.ContentZhTW},
|
||||
{i18n.LangEn, a.ContentEn},
|
||||
{i18n.LangJa, a.ContentJa},
|
||||
} {
|
||||
if l.content != "" {
|
||||
langs = append(langs, l.code)
|
||||
}
|
||||
}
|
||||
return langs
|
||||
}
|
||||
|
||||
// 支持的时间格式。裸时间(不带时区偏移)按 [announcement] timezone 解释。
|
||||
var announcementTimeLayouts = []string{
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02 15:04:05",
|
||||
@@ -35,21 +125,6 @@ var announcementTimeLayouts = []string{
|
||||
time.RFC3339,
|
||||
}
|
||||
|
||||
// Active 判断给定时刻是否处于公告展示窗口,并返回公告正文。
|
||||
// 窗口为左闭右开 [Start, End),两侧留空表示不限。
|
||||
func (a announcementConfig) Active(now time.Time) (string, bool) {
|
||||
if a.Content == "" {
|
||||
return "", false
|
||||
}
|
||||
if !a.StartAt.IsZero() && now.Before(a.StartAt) {
|
||||
return "", false
|
||||
}
|
||||
if !a.EndAt.IsZero() && !now.Before(a.EndAt) {
|
||||
return "", false
|
||||
}
|
||||
return a.Content, true
|
||||
}
|
||||
|
||||
// sanitizeAnnouncement 解析公告时间窗。
|
||||
//
|
||||
// 时间写错只告警并关闭公告,不阻断启动:公告是展示性配置,拼错一个日期不该让整站
|
||||
@@ -57,7 +132,11 @@ func (a announcementConfig) Active(now time.Time) (string, bool) {
|
||||
// 忽略属于同一类坑(见 unknown.go)。
|
||||
func sanitizeAnnouncement() {
|
||||
Announcement.Content = strings.TrimSpace(Announcement.Content)
|
||||
if Announcement.Content == "" {
|
||||
Announcement.ContentZhCN = strings.TrimSpace(Announcement.ContentZhCN)
|
||||
Announcement.ContentZhTW = strings.TrimSpace(Announcement.ContentZhTW)
|
||||
Announcement.ContentEn = strings.TrimSpace(Announcement.ContentEn)
|
||||
Announcement.ContentJa = strings.TrimSpace(Announcement.ContentJa)
|
||||
if !Announcement.hasContent() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,12 +150,19 @@ func sanitizeAnnouncement() {
|
||||
Announcement = announcementConfig{}
|
||||
}
|
||||
|
||||
loc, err := loadAnnouncementLocation(Announcement.Timezone)
|
||||
if err != nil {
|
||||
disable("timezone", Announcement.Timezone, "不是合法的 IANA 时区名(如 Asia/Shanghai / UTC)")
|
||||
return
|
||||
}
|
||||
Announcement.loc = loc
|
||||
|
||||
var ok bool
|
||||
if Announcement.StartAt, ok = parseAnnouncementTime(Announcement.Start); !ok {
|
||||
if Announcement.StartAt, ok = parseAnnouncementTime(Announcement.Start, loc); !ok {
|
||||
disable("start", Announcement.Start, "支持 2006-01-02 15:04 / 2006-01-02 15:04:05 / 2006-01-02 / RFC3339")
|
||||
return
|
||||
}
|
||||
if Announcement.EndAt, ok = parseAnnouncementTime(Announcement.End); !ok {
|
||||
if Announcement.EndAt, ok = parseAnnouncementTime(Announcement.End, loc); !ok {
|
||||
disable("end", Announcement.End, "支持 2006-01-02 15:04 / 2006-01-02 15:04:05 / 2006-01-02 / RFC3339")
|
||||
return
|
||||
}
|
||||
@@ -85,23 +171,33 @@ func sanitizeAnnouncement() {
|
||||
return
|
||||
}
|
||||
|
||||
_, activeNow := Announcement.Active(time.Now())
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"zone": loc.String(),
|
||||
"languages": strings.Join(Announcement.configuredLanguages(), ","),
|
||||
"start": announcementBoundLabel(Announcement.StartAt),
|
||||
"end": announcementBoundLabel(Announcement.EndAt),
|
||||
"zone": time.Local.String(),
|
||||
"active_now": activeNow,
|
||||
"active_now": Announcement.inWindow(time.Now()),
|
||||
}).Info("站内公告已配置")
|
||||
}
|
||||
|
||||
// loadAnnouncementLocation 解析时区名;空值取默认时区(东八区)。
|
||||
func loadAnnouncementLocation(name string) (*time.Location, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
name = defaultAnnouncementZone
|
||||
}
|
||||
return time.LoadLocation(name)
|
||||
}
|
||||
|
||||
// parseAnnouncementTime 解析单个时间点;空串返回零值(表示不限)且视为合法。
|
||||
func parseAnnouncementTime(raw string) (time.Time, bool) {
|
||||
// 带偏移的 RFC3339 以自带偏移为准,loc 只用于裸时间。
|
||||
func parseAnnouncementTime(raw string, loc *time.Location) (time.Time, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return time.Time{}, true
|
||||
}
|
||||
for _, layout := range announcementTimeLayouts {
|
||||
if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil {
|
||||
if t, err := time.ParseInLocation(layout, raw, loc); err == nil {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
@@ -109,7 +205,7 @@ func parseAnnouncementTime(raw string) (time.Time, bool) {
|
||||
}
|
||||
|
||||
// announcementBoundLabel 把时间点格式化成带时区偏移的字符串,供启动日志核对
|
||||
// 「我写的 10:00 到底被当成了几点」——裸时间按服务器本地时区解释,容器多为 UTC。
|
||||
// 「我写的 10:00 到底被当成了几点」—— 裸时间按 [announcement] timezone 解释。
|
||||
func announcementBoundLabel(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "不限"
|
||||
|
||||
@@ -7,9 +7,12 @@ package conf
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
)
|
||||
|
||||
// setAnnouncement 在测试期间覆盖全局公告配置,结束后恢复。
|
||||
// 不设 Timezone,即走默认的东八区。
|
||||
func setAnnouncement(t *testing.T, content, start, end string) {
|
||||
t.Helper()
|
||||
old := Announcement
|
||||
@@ -18,9 +21,22 @@ func setAnnouncement(t *testing.T, content, start, end string) {
|
||||
sanitizeAnnouncement()
|
||||
}
|
||||
|
||||
func mustParse(t *testing.T, layout, value string) time.Time {
|
||||
// setAnnouncementConfig 用完整配置覆盖,供分语言 / 时区用例使用。
|
||||
func setAnnouncementConfig(t *testing.T, cfg announcementConfig) {
|
||||
t.Helper()
|
||||
parsed, err := time.ParseInLocation(layout, value, time.Local)
|
||||
old := Announcement
|
||||
t.Cleanup(func() { Announcement = old })
|
||||
Announcement = cfg
|
||||
sanitizeAnnouncement()
|
||||
}
|
||||
|
||||
func mustParseIn(t *testing.T, layout, value string) time.Time {
|
||||
t.Helper()
|
||||
loc, err := time.LoadLocation(defaultAnnouncementZone)
|
||||
if err != nil {
|
||||
t.Fatalf("load zone %s: %v", defaultAnnouncementZone, err)
|
||||
}
|
||||
parsed, err := time.ParseInLocation(layout, value, loc)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %q: %v", value, err)
|
||||
}
|
||||
@@ -30,16 +46,16 @@ func mustParse(t *testing.T, layout, value string) time.Time {
|
||||
func TestAnnouncementDisabledWhenContentEmpty(t *testing.T) {
|
||||
setAnnouncement(t, " ", "2026-10-24 00:00", "2026-10-26 00:00")
|
||||
|
||||
if _, ok := Announcement.Active(time.Now()); ok {
|
||||
t.Fatal("content 为空时公告应关闭")
|
||||
if _, ok := Announcement.Active(time.Now(), i18n.LangZhCN); ok {
|
||||
t.Fatal("所有语言文案都为空时公告应关闭")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnouncementWindowIsHalfOpen(t *testing.T) {
|
||||
setAnnouncement(t, "维护通知", "2026-10-24 00:00", "2026-10-26 00:00")
|
||||
|
||||
start := mustParse(t, "2006-01-02 15:04", "2026-10-24 00:00")
|
||||
end := mustParse(t, "2006-01-02 15:04", "2026-10-26 00:00")
|
||||
start := mustParseIn(t, "2006-01-02 15:04", "2026-10-24 00:00")
|
||||
end := mustParseIn(t, "2006-01-02 15:04", "2026-10-26 00:00")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -54,7 +70,7 @@ func TestAnnouncementWindowIsHalfOpen(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
content, ok := Announcement.Active(c.now)
|
||||
content, ok := Announcement.Active(c.now, i18n.LangZhCN)
|
||||
if ok != c.want {
|
||||
t.Fatalf("Active(%s) = %v, want %v", c.now, ok, c.want)
|
||||
}
|
||||
@@ -67,33 +83,71 @@ func TestAnnouncementWindowIsHalfOpen(t *testing.T) {
|
||||
|
||||
// 只写一侧的边界必须保持「该侧不限」,不能因为另一侧为空就整体失效。
|
||||
func TestAnnouncementOneSidedWindow(t *testing.T) {
|
||||
now := time.Date(2026, 10, 22, 12, 0, 0, 0, time.Local)
|
||||
now := mustParseIn(t, "2006-01-02 15:04", "2026-10-22 12:00")
|
||||
|
||||
setAnnouncement(t, "只有开始", "2026-10-23 00:00", "")
|
||||
if _, ok := Announcement.Active(now); ok {
|
||||
if _, ok := Announcement.Active(now, i18n.LangZhCN); ok {
|
||||
t.Fatal("只写 start 且尚未到点时应不展示")
|
||||
}
|
||||
if _, ok := Announcement.Active(now.Add(48 * time.Hour)); !ok {
|
||||
if _, ok := Announcement.Active(now.Add(48*time.Hour), i18n.LangZhCN); !ok {
|
||||
t.Fatal("只写 start 时到点后应一直展示")
|
||||
}
|
||||
|
||||
setAnnouncement(t, "只有结束", "", "2026-10-23 00:00")
|
||||
if _, ok := Announcement.Active(now); !ok {
|
||||
if _, ok := Announcement.Active(now, i18n.LangZhCN); !ok {
|
||||
t.Fatal("只写 end 且未到点时应立即展示")
|
||||
}
|
||||
if _, ok := Announcement.Active(now.Add(48 * time.Hour)); ok {
|
||||
if _, ok := Announcement.Active(now.Add(48*time.Hour), i18n.LangZhCN); ok {
|
||||
t.Fatal("只写 end 时过点后应下线")
|
||||
}
|
||||
|
||||
setAnnouncement(t, "一直展示", "", "")
|
||||
if _, ok := Announcement.Active(now.AddDate(5, 0, 0)); !ok {
|
||||
if _, ok := Announcement.Active(now.AddDate(5, 0, 0), i18n.LangZhCN); !ok {
|
||||
t.Fatal("起止都留空时应一直展示")
|
||||
}
|
||||
}
|
||||
|
||||
// 分语言文案:配了就用自己的,没配回退 content;content 也没配则该语言不显示。
|
||||
func TestAnnouncementLanguageSelection(t *testing.T) {
|
||||
setAnnouncementConfig(t, announcementConfig{
|
||||
Content: "fallback",
|
||||
ContentZhCN: "简中",
|
||||
ContentEn: "english",
|
||||
ContentJa: "日本語",
|
||||
})
|
||||
|
||||
cases := []struct{ lang, want string }{
|
||||
{i18n.LangZhCN, "简中"},
|
||||
{i18n.LangEn, "english"},
|
||||
{i18n.LangJa, "日本語"},
|
||||
{i18n.LangZhTW, "fallback"}, // 没配繁中 → 回退默认文案
|
||||
{"en-US", "english"}, // Resolve 会归一化
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, ok := Announcement.Active(time.Now(), c.lang)
|
||||
if !ok || got != c.want {
|
||||
t.Fatalf("Active(lang=%s) = %q ok=%v, want %q", c.lang, got, ok, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
// 只配英文、content 留空:简中访客不应看到英文公告。
|
||||
setAnnouncementConfig(t, announcementConfig{ContentEn: "english only"})
|
||||
if _, ok := Announcement.Active(time.Now(), i18n.LangZhCN); ok {
|
||||
t.Fatal("该语言未配文案且无默认文案时不应展示")
|
||||
}
|
||||
if got, ok := Announcement.Active(time.Now(), i18n.LangEn); !ok || got != "english only" {
|
||||
t.Fatalf("英文应正常展示,got %q ok=%v", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnouncementTimeFormats(t *testing.T) {
|
||||
// 带偏移的 RFC3339 必须按字面时刻解析,而不是再套一层本地时区。
|
||||
at, ok := parseAnnouncementTime("2026-10-25T02:00:00+08:00")
|
||||
loc, err := time.LoadLocation(defaultAnnouncementZone)
|
||||
if err != nil {
|
||||
t.Fatalf("load zone: %v", err)
|
||||
}
|
||||
|
||||
// 带偏移的 RFC3339 必须按字面时刻解析,而不是再套一层默认时区。
|
||||
at, ok := parseAnnouncementTime("2026-10-25T02:00:00+08:00", loc)
|
||||
if !ok {
|
||||
t.Fatal("RFC3339 应可解析")
|
||||
}
|
||||
@@ -102,20 +156,72 @@ func TestAnnouncementTimeFormats(t *testing.T) {
|
||||
t.Fatalf("解析结果 = %s, want %s", at.UTC(), want)
|
||||
}
|
||||
|
||||
if at, ok := parseAnnouncementTime("2026-10-25"); !ok || at.Hour() != 0 {
|
||||
if at, ok := parseAnnouncementTime("2026-10-25", loc); !ok || at.Hour() != 0 {
|
||||
t.Fatalf("纯日期应解析为当天 00:00,got %s ok=%v", at, ok)
|
||||
}
|
||||
if at, ok := parseAnnouncementTime("2026-10-25 02:00:59"); !ok || at.Second() != 59 {
|
||||
if at, ok := parseAnnouncementTime("2026-10-25 02:00:59", loc); !ok || at.Second() != 59 {
|
||||
t.Fatalf("秒级格式应可解析,got %s ok=%v", at, ok)
|
||||
}
|
||||
if _, ok := parseAnnouncementTime("2026/10/25 02:00"); ok {
|
||||
if _, ok := parseAnnouncementTime("2026/10/25 02:00", loc); ok {
|
||||
t.Fatal("错误格式应解析失败")
|
||||
}
|
||||
if _, ok := parseAnnouncementTime("2026-13-01"); ok {
|
||||
if _, ok := parseAnnouncementTime("2026-13-01", loc); ok {
|
||||
t.Fatal("非法月份应解析失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 裸时间必须按东八区解释:运维写「10-25 02:00」指北京时间,
|
||||
// 而容器里的 time.Local 是 UTC(旧实现整体偏 8 小时)。
|
||||
func TestAnnouncementBareTimeUsesBeijing(t *testing.T) {
|
||||
setAnnouncement(t, "维护通知", "2026-10-25 02:00", "")
|
||||
|
||||
want := time.Date(2026, 10, 24, 18, 0, 0, 0, time.UTC) // 北京 10-25 02:00
|
||||
if !Announcement.StartAt.Equal(want) {
|
||||
t.Fatalf("裸时间应按 +08:00 解释,got %s, want %s", Announcement.StartAt.UTC(), want)
|
||||
}
|
||||
if _, offset := Announcement.StartAt.Zone(); offset != 8*60*60 {
|
||||
t.Fatalf("解析后的偏移 = %d 秒, want 28800", offset)
|
||||
}
|
||||
}
|
||||
|
||||
// timezone 显式指定时以它为准,不能继续套东八区。
|
||||
func TestAnnouncementTimezoneOverride(t *testing.T) {
|
||||
setAnnouncementConfig(t, announcementConfig{
|
||||
Content: "维护通知",
|
||||
Start: "2026-10-25 00:00",
|
||||
Timezone: "UTC",
|
||||
})
|
||||
|
||||
want := time.Date(2026, 10, 25, 0, 0, 0, 0, time.UTC)
|
||||
if !Announcement.StartAt.Equal(want) {
|
||||
t.Fatalf("timezone=UTC 时应按 UTC 解析,got %s", Announcement.StartAt.UTC())
|
||||
}
|
||||
|
||||
setAnnouncementConfig(t, announcementConfig{
|
||||
Content: "维护通知",
|
||||
Start: "2026-10-25 00:00",
|
||||
Timezone: "America/New_York",
|
||||
})
|
||||
// 纽约 10-25 00:00 = UTC 10-25 04:00(EDT,UTC-4)
|
||||
wantNY := time.Date(2026, 10, 25, 4, 0, 0, 0, time.UTC)
|
||||
if !Announcement.StartAt.Equal(wantNY) {
|
||||
t.Fatalf("timezone=America/New_York 解析错误,got %s", Announcement.StartAt.UTC())
|
||||
}
|
||||
}
|
||||
|
||||
// 时区名写错和日期写错一样:只关公告,不阻断启动。
|
||||
func TestAnnouncementInvalidTimezoneDisables(t *testing.T) {
|
||||
setAnnouncementConfig(t, announcementConfig{
|
||||
Content: "维护通知",
|
||||
Start: "2026-10-25 00:00",
|
||||
Timezone: "Mars/Olympus",
|
||||
})
|
||||
|
||||
if Announcement.Content != "" {
|
||||
t.Fatal("时区无效时公告应被关闭")
|
||||
}
|
||||
}
|
||||
|
||||
// 时间写错只关公告,不能让站点起不来;同时必须清空解析结果,避免残留时间窗。
|
||||
func TestAnnouncementInvalidTimeDisables(t *testing.T) {
|
||||
for _, c := range []struct{ name, start, end string }{
|
||||
|
||||
@@ -218,6 +218,9 @@ func ensureFrequentlyUsedIndexes(db *gorm.DB) error {
|
||||
indexes := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_questions_asker_user_id ON questions(asker_user_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_questions_user_private_created ON questions(user_id, is_private, created_at)`,
|
||||
// 「下一条未读」:user_id + is_read 过滤后按 created_at 取最早一条。
|
||||
// 上面那条中间隔着 is_private,帮不上这个排序。
|
||||
`CREATE INDEX IF NOT EXISTS idx_questions_user_unread_created ON questions(user_id, is_read, created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_users_show_in_home_created ON users(show_in_home, created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_votes_status_visibility_created ON votes(status, visibility, created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_messages_sender_created ON messages(sender_id, created_at)`,
|
||||
|
||||
@@ -67,8 +67,9 @@ func FuncMap() []template.FuncMap {
|
||||
},
|
||||
// announcement 返回当前应展示的公告(已转义的安全 HTML),窗口外返回空串。
|
||||
// 时间窗判定放在服务端:不依赖访客时钟,也不会先渲染出来再被脚本抹掉。
|
||||
"announcement": func() template.HTML {
|
||||
content, ok := conf.Announcement.Active(time.Now())
|
||||
// 按访客语言取文案,未翻译的语言回退到 content。
|
||||
"announcement": func(lang string) template.HTML {
|
||||
content, ok := conf.Announcement.Active(time.Now(), lang)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user