// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package conf import ( "strings" "time" "github.com/sirupsen/logrus" ) // announcementConfig 站内公告([announcement] 段)。 // // Content 留空即关闭公告;Start / End 留空表示该侧不限。 // 展示窗口是左闭右开 [Start, End):到 end 那一刻公告消失, // 运维不必为了「准时下线」把时间掐到秒后还担心多挂一分钟。 type announcementConfig struct { Content string `ini:"content"` Start string `ini:"start"` End string `ini:"end"` // 解析后的时间点,零值表示该侧不限。 // ini tag 为 "-":既不参与 ini 解析,也不会被未知键检测登记为合法键名。 StartAt time.Time `ini:"-"` EndAt time.Time `ini:"-"` } // 支持的时间格式。裸时间(不带时区偏移)按服务器本地时区解释。 var announcementTimeLayouts = []string{ "2006-01-02 15:04", "2006-01-02 15:04:05", "2006-01-02", 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 解析公告时间窗。 // // 时间写错只告警并关闭公告,不阻断启动:公告是展示性配置,拼错一个日期不该让整站 // 502;但必须留下能定位的日志 —— 否则现象就是「配了却不显示」,和未知键被静默 // 忽略属于同一类坑(见 unknown.go)。 func sanitizeAnnouncement() { Announcement.Content = strings.TrimSpace(Announcement.Content) if Announcement.Content == "" { return } disable := func(key, value, reason string) { logrus.WithFields(logrus.Fields{ "section": "announcement", "key": key, "value": value, "reason": reason, }).Warn("公告配置无效,已关闭公告(站点其它功能不受影响)") Announcement = announcementConfig{} } var ok bool if Announcement.StartAt, ok = parseAnnouncementTime(Announcement.Start); !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 { disable("end", Announcement.End, "支持 2006-01-02 15:04 / 2006-01-02 15:04:05 / 2006-01-02 / RFC3339") return } if !Announcement.StartAt.IsZero() && !Announcement.EndAt.IsZero() && !Announcement.EndAt.After(Announcement.StartAt) { disable("end", Announcement.End, "end 必须晚于 start,否则公告永远不会展示") return } _, activeNow := Announcement.Active(time.Now()) logrus.WithFields(logrus.Fields{ "start": announcementBoundLabel(Announcement.StartAt), "end": announcementBoundLabel(Announcement.EndAt), "zone": time.Local.String(), "active_now": activeNow, }).Info("站内公告已配置") } // parseAnnouncementTime 解析单个时间点;空串返回零值(表示不限)且视为合法。 func parseAnnouncementTime(raw string) (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 { return t, true } } return time.Time{}, false } // announcementBoundLabel 把时间点格式化成带时区偏移的字符串,供启动日志核对 // 「我写的 10:00 到底被当成了几点」——裸时间按服务器本地时区解释,容器多为 UTC。 func announcementBoundLabel(t time.Time) string { if t.IsZero() { return "不限" } return t.Format("2006-01-02 15:04:05 -07:00") }