From 5dfa1550fbe52958646ca0d2ce4c8b52c629e395 Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Sun, 27 Sep 2026 03:34:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=EF=BC=9A=E7=AB=99=E7=82=B9?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E6=96=B0=E5=A2=9E=E7=A4=BE=E4=BA=A4=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E4=B8=8E=E8=87=AA=E5=AE=9A=E4=B9=89=20JS=20=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit social_links 以 JSON 数组存进 settings KV([{label,url}]),解码容错: 空串 / 坏 JSON / 缺 label 或 url 的条目一律丢弃,前台拿到空数组时区块 自动隐藏。custom_js 原样存取——它是站主自己的代码(统计脚本等), 做转义只会把脚本洗坏。后台表单与前台展示在下一笔前端提交里。 --- backend/internal/model/model.go | 15 +++++++++++ backend/internal/store/store.go | 40 +++++++++++++++++++++++++++- backend/internal/store/store_test.go | 1 - 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/backend/internal/model/model.go b/backend/internal/model/model.go index 4f84cee..caf3a31 100644 --- a/backend/internal/model/model.go +++ b/backend/internal/model/model.go @@ -120,6 +120,13 @@ type MonthBucket struct { Count int `json:"count"` } +// SocialLink 是站主在后台填写的社交 / 源码入口(label + url), +// 前台渲染成图标或文字链接。icon 不入库 —— 由前端按 url 域名推导。 +type SocialLink struct { + Label string `json:"label"` + URL string `json:"url"` +} + type Settings struct { SiteTitle string `json:"site_title"` SiteDesc string `json:"site_desc"` @@ -128,6 +135,9 @@ type Settings struct { FooterNote string `json:"footer_note"` ICPLicense string `json:"icp"` PostsPerPage int `json:"posts_per_page"` + // SocialLinks 以 JSON 数组形式存在 settings KV 里(key: social_links), + // 解析失败/为空时前台拿到空数组,区块自动隐藏。 + SocialLinks []SocialLink `json:"social_links"` // LightSkinID is the front-end skin used when the client (or system) // prefers light. Valid values: paper / sage / rose. // Dark side is fixed to ink for now — kept implicit so we can add @@ -147,4 +157,9 @@ type Settings struct { // Only injected for the vivid UI, and never on /admin. Always non-nil so // the JSON response is {} rather than null. CustomCSS map[string]string `json:"custom_css"` + // CustomJS holds owner-authored JavaScript (analytics snippets like + // Google Analytics / Plausible / Umami). Injected on every public page + // of either UI, never on /admin. Stored raw — it's the owner's own code, + // sanitizing it would only break the snippet. + CustomJS string `json:"custom_js"` } diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go index 2413e6d..ef420be 100644 --- a/backend/internal/store/store.go +++ b/backend/internal/store/store.go @@ -193,12 +193,46 @@ func settingsFromMap(m map[string]string) model.Settings { st.ThemeID = st.LightSkinID st.UIID = sanitizeUI(m["ui_id"]) st.CustomCSS = decodeCSSMap(m["custom_css"]) + st.CustomJS = m["custom_js"] + st.SocialLinks = decodeSocialLinks(m["social_links"]) if n := atoi(m["posts_per_page"]); n > 0 { st.PostsPerPage = n } return st } +// decodeSocialLinks 把 KV 里的 JSON 数组还原成社交链接。容错:空串、坏 JSON、 +// 缺 label/url 的条目一律丢弃,返回空切片(前台区块自动隐藏)。 +func decodeSocialLinks(s string) []model.SocialLink { + out := []model.SocialLink{} + if strings.TrimSpace(s) == "" { + return out + } + if err := json.Unmarshal([]byte(s), &out); err != nil { + return []model.SocialLink{} + } + clean := make([]model.SocialLink, 0, len(out)) + for _, l := range out { + l.Label = strings.TrimSpace(l.Label) + l.URL = strings.TrimSpace(l.URL) + if l.Label != "" && l.URL != "" { + clean = append(clean, l) + } + } + return clean +} + +func encodeSocialLinks(ls []model.SocialLink) string { + if len(ls) == 0 { + return "" + } + b, err := json.Marshal(ls) + if err != nil { + return "" + } + return string(b) +} + func atoi(v string) int { n := 0 for _, r := range v { @@ -341,6 +375,10 @@ func (s *Store) UpdateSettings(st model.Settings) error { "ui_id": st.UIID, // Full replace: an omitted/empty map clears every section's CSS. "custom_css": encodeCSSMap(st.CustomCSS), + // 原样存:站主自己的代码,不做任何转义/清洗。 + "custom_js": st.CustomJS, + // 空数组存空串:KV 里不留 "null"。 + "social_links": encodeSocialLinks(st.SocialLinks), } for k, v := range sets { if s.db.Dialect == db.Postgres { @@ -522,7 +560,7 @@ func (s *Store) Get(id int64) (model.Post, error) { // 返回顺序与时间线一致(published_at DESC):最旧在最前、最新在最后, // 目标文章夹在中间,前后各取最多 2 篇。 func (s *Store) Neighbors(slug string) ([]model.Post, error) { - rows, err := s.db.Query(s.db.Q(`SELECT ` + listCols + ` FROM posts + rows, err := s.db.Query(s.db.Q(`SELECT `+listCols+` FROM posts WHERE status = ? ORDER BY published_at DESC`), model.StatusPublished) if err != nil { return nil, err diff --git a/backend/internal/store/store_test.go b/backend/internal/store/store_test.go index b39f584..33c665f 100644 --- a/backend/internal/store/store_test.go +++ b/backend/internal/store/store_test.go @@ -490,4 +490,3 @@ func TestProjectCRUD(t *testing.T) { t.Errorf("after delete: got %v, want ErrNotFound", err) } } -