package api import ( "fmt" "net/http" "net/url" "strings" "home-vue-go/internal/config" "home-vue-go/internal/database" "github.com/gin-gonic/gin" ) type updateSiteConfigRequest struct { SiteName *string `json:"siteName"` SiteURL *string `json:"siteURL"` SiteIcon *string `json:"siteIcon"` SiteDescription *string `json:"siteDescription"` SiteKeywords *string `json:"siteKeywords"` UserName *string `json:"userName"` ProfileImageURL *string `json:"profileImageURL"` ICPNumber *string `json:"icpNumber"` PoliceNumber *string `json:"policeNumber"` PageTitle *string `json:"pageTitle"` Favicon *string `json:"favicon"` IconLibrary *string `json:"iconLibrary"` FontLibrary *string `json:"fontLibrary"` FooterYearStart *string `json:"footerYearStart"` FooterYearEnd *string `json:"footerYearEnd"` ShowVisitTimer *bool `json:"showVisitTimer"` RotatingTexts *[]string `json:"rotatingTexts"` GreetingText *string `json:"greetingText"` OnlineStatusText *string `json:"onlineStatusText"` FooterLabel *string `json:"footerLabel"` ShowAbout *bool `json:"showAbout"` ShowSites *bool `json:"showSites"` ShowContacts *bool `json:"showContacts"` ShowThemeToggle *bool `json:"showThemeToggle"` ShowFooter *bool `json:"showFooter"` AboutTitle *string `json:"aboutTitle"` AboutDescription *string `json:"aboutDescription"` AboutLinks *[]config.AboutLink `json:"aboutLinks"` SitePageSize *int `json:"sitePageSize"` OpenLinksNewTab *bool `json:"openLinksInNewTab"` AnalyticsProvider *string `json:"analyticsProvider"` UmamiScript *string `json:"umamiScript"` UmamiScriptURL *string `json:"umamiScriptUrl"` UmamiWebsiteID *string `json:"umamiWebsiteId"` UmamiAPIMode *string `json:"umamiApiMode"` UmamiAPIURL *string `json:"umamiApiUrl"` UmamiCredential *string `json:"umamiCredential"` ClearUmamiCredential *bool `json:"clearUmamiCredential"` UmamiDomains *string `json:"umamiDomains"` UmamiDoNotTrack *bool `json:"umamiDoNotTrack"` UmamiExcludeSearch *bool `json:"umamiExcludeSearch"` UmamiExcludeHash *bool `json:"umamiExcludeHash"` UmamiPerformance *bool `json:"umamiPerformance"` UmamiTag *string `json:"umamiTag"` UmamiTrackerOptions *struct { Domains *string `json:"domains"` DoNotTrack *bool `json:"doNotTrack"` ExcludeSearch *bool `json:"excludeSearch"` ExcludeHash *bool `json:"excludeHash"` Performance *bool `json:"performance"` Tag *string `json:"tag"` } `json:"umamiTrackerOptions"` } func GetSiteConfig(db *database.Database) gin.HandlerFunc { return func(c *gin.Context) { settings, err := db.LoadSiteSettings(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) return } c.JSON(http.StatusOK, siteConfigResponse(settings, false)) } } func GetAdminSiteConfig(db *database.Database) gin.HandlerFunc { return func(c *gin.Context) { settings, err := db.LoadSiteSettings(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) return } c.JSON(http.StatusOK, siteConfigResponse(settings, true)) } } func UpdateSiteConfig(db *database.Database, cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { var req updateSiteConfigRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "配置参数格式错误"}) return } ctx := c.Request.Context() settings, err := db.LoadSiteSettings(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "加载现有站点配置失败"}) return } next := settings.Clone() applyString(&next.SiteName, req.SiteName) applyString(&next.SiteURL, req.SiteURL) applyString(&next.SiteIcon, req.SiteIcon) applyString(&next.SiteDescription, req.SiteDescription) applyString(&next.SiteKeywords, req.SiteKeywords) applyString(&next.UserName, req.UserName) applyString(&next.ProfileImageURL, req.ProfileImageURL) applyString(&next.ICPNumber, req.ICPNumber) applyString(&next.PoliceNumber, req.PoliceNumber) applyString(&next.PageTitle, req.PageTitle) applyString(&next.Favicon, req.Favicon) applyString(&next.IconLibrary, req.IconLibrary) applyString(&next.FontLibrary, req.FontLibrary) applyString(&next.FooterYearStart, req.FooterYearStart) applyString(&next.FooterYearEnd, req.FooterYearEnd) applyBool(&next.ShowVisitTimer, req.ShowVisitTimer) applySlice(&next.RotatingTexts, req.RotatingTexts) applyString(&next.GreetingText, req.GreetingText) applyString(&next.OnlineStatusText, req.OnlineStatusText) applyString(&next.FooterLabel, req.FooterLabel) applyBool(&next.ShowAbout, req.ShowAbout) applyBool(&next.ShowSites, req.ShowSites) applyBool(&next.ShowContacts, req.ShowContacts) applyBool(&next.ShowThemeToggle, req.ShowThemeToggle) applyBool(&next.ShowFooter, req.ShowFooter) applyString(&next.AboutTitle, req.AboutTitle) applyString(&next.AboutDescription, req.AboutDescription) applySlice(&next.AboutLinks, req.AboutLinks) applyInt(&next.SitePageSize, req.SitePageSize) applyBool(&next.OpenLinksNewTab, req.OpenLinksNewTab) applyString(&next.AnalyticsProvider, req.AnalyticsProvider) applyString(&next.UmamiScript, req.UmamiScript) applyString(&next.UmamiScript, req.UmamiScriptURL) applyString(&next.UmamiWebsiteID, req.UmamiWebsiteID) applyString(&next.UmamiAPIMode, req.UmamiAPIMode) applyString(&next.UmamiAPIURL, req.UmamiAPIURL) applyString(&next.UmamiDomains, req.UmamiDomains) applyBool(&next.UmamiDoNotTrack, req.UmamiDoNotTrack) applyBool(&next.UmamiExcludeSearch, req.UmamiExcludeSearch) applyBool(&next.UmamiExcludeHash, req.UmamiExcludeHash) applyBool(&next.UmamiPerformance, req.UmamiPerformance) applyString(&next.UmamiTag, req.UmamiTag) if options := req.UmamiTrackerOptions; options != nil { applyString(&next.UmamiDomains, options.Domains) applyBool(&next.UmamiDoNotTrack, options.DoNotTrack) applyBool(&next.UmamiExcludeSearch, options.ExcludeSearch) applyBool(&next.UmamiExcludeHash, options.ExcludeHash) applyBool(&next.UmamiPerformance, options.Performance) applyString(&next.UmamiTag, options.Tag) } if req.ClearUmamiCredential != nil && *req.ClearUmamiCredential { next.UmamiCredential = "" } else if req.UmamiCredential != nil { if strings.TrimSpace(*req.UmamiCredential) == "" { next.UmamiCredential = "" } else { if cfg == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "加密配置不可用"}) return } encrypted, err := cfg.EncryptSecret(strings.TrimSpace(*req.UmamiCredential)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "保存统计凭据失败"}) return } next.UmamiCredential = encrypted } } if err := validateSiteSettings(next); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := db.SaveSiteSettings(ctx, next); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点配置失败"}) return } c.JSON(http.StatusOK, siteConfigResponse(next, true)) } } func GetRotatingTexts(db *database.Database) gin.HandlerFunc { return func(c *gin.Context) { settings, err := db.LoadSiteSettings(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "加载轮换文本配置失败"}) return } c.JSON(http.StatusOK, gin.H{"texts": settings.RotatingTexts}) } } func UpdateRotatingTexts(db *database.Database) gin.HandlerFunc { return func(c *gin.Context) { var req struct { Texts []string `json:"texts"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "轮换文本参数格式错误"}) return } settings, err := db.LoadSiteSettings(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) return } settings.RotatingTexts = req.Texts settings.Normalize() if err := db.SaveSiteSettings(c.Request.Context(), settings); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "保存轮换文本配置失败"}) return } c.JSON(http.StatusOK, gin.H{"message": "轮换文本配置已保存", "texts": settings.RotatingTexts}) } } func siteConfigResponse(settings *config.SiteSettings, admin bool) gin.H { result := gin.H{ "siteName": settings.SiteName, "siteURL": settings.SiteURL, "siteIcon": settings.SiteIcon, "siteDescription": settings.SiteDescription, "siteKeywords": settings.SiteKeywords, "userName": settings.UserName, "profileImageURL": settings.ProfileImageURL, "icpNumber": settings.ICPNumber, "policeNumber": settings.PoliceNumber, "pageTitle": settings.PageTitle, "favicon": settings.Favicon, "iconLibrary": settings.IconLibrary, "fontLibrary": settings.FontLibrary, "footerYearStart": settings.FooterYearStart, "footerYearEnd": settings.FooterYearEnd, "showVisitTimer": settings.ShowVisitTimer, "rotatingTexts": settings.RotatingTexts, "greetingText": settings.GreetingText, "onlineStatusText": settings.OnlineStatusText, "footerLabel": settings.FooterLabel, "showAbout": settings.ShowAbout, "showSites": settings.ShowSites, "showContacts": settings.ShowContacts, "showThemeToggle": settings.ShowThemeToggle, "showFooter": settings.ShowFooter, "aboutTitle": settings.AboutTitle, "aboutDescription": settings.AboutDescription, "aboutLinks": settings.AboutLinks, "sitePageSize": settings.SitePageSize, "openLinksInNewTab": settings.OpenLinksNewTab, "analyticsProvider": settings.AnalyticsProvider, "umamiScript": settings.UmamiScript, "umamiScriptUrl": settings.UmamiScript, "umamiWebsiteId": settings.UmamiWebsiteID, "umamiDomains": settings.UmamiDomains, "umamiDoNotTrack": settings.UmamiDoNotTrack, "umamiExcludeSearch": settings.UmamiExcludeSearch, "umamiExcludeHash": settings.UmamiExcludeHash, "umamiPerformance": settings.UmamiPerformance, "umamiTag": settings.UmamiTag, "umamiTrackerOptions": gin.H{"domains": settings.UmamiDomains, "doNotTrack": settings.UmamiDoNotTrack, "excludeSearch": settings.UmamiExcludeSearch, "excludeHash": settings.UmamiExcludeHash, "performance": settings.UmamiPerformance, "tag": settings.UmamiTag}, } if admin { result["umamiApiMode"] = settings.UmamiAPIMode result["umamiApiUrl"] = settings.UmamiAPIURL result["umamiCredentialConfigured"] = settings.UmamiCredential != "" } return result } func validateSiteSettings(settings *config.SiteSettings) error { for name, value := range map[string]string{"站点URL": settings.SiteURL, "Umami脚本地址": settings.UmamiScript, "Umami API地址": settings.UmamiAPIURL} { if value == "" { continue } parsed, err := url.ParseRequestURI(value) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { return fmt.Errorf("%s必须是有效的 http/https 地址", name) } } if settings.SitePageSize != 6 && settings.SitePageSize != 9 && settings.SitePageSize != 12 { return fmt.Errorf("站点每页数量只能是 6、9 或 12") } if settings.AnalyticsProvider != "local" && settings.AnalyticsProvider != "umami" { return fmt.Errorf("统计来源只能是 local 或 umami") } if settings.UmamiAPIMode != "selfhost" && settings.UmamiAPIMode != "cloud" { return fmt.Errorf("Umami API 模式不正确") } if len(settings.AboutLinks) > 8 { return fmt.Errorf("关于链接最多支持 8 条") } for _, link := range settings.AboutLinks { if strings.TrimSpace(link.URL) == "" { return fmt.Errorf("关于链接地址不能为空") } parsed, err := url.ParseRequestURI(link.URL) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { return fmt.Errorf("关于链接必须是有效的 http/https 地址") } } return nil } func applyString(target *string, value *string) { if value != nil { *target = strings.TrimSpace(*value) } } func applyBool(target *bool, value *bool) { if value != nil { *target = *value } } func applyInt(target *int, value *int) { if value != nil { *target = *value } } func applySlice[T any](target *[]T, value *[]T) { if value != nil { *target = *value } }