使用了AI重构项目,并完善了一部分后台问题

This commit is contained in:
2026-08-05 04:47:11 +08:00
parent 146b6b1e6c
commit ac740c24fd
35 changed files with 2876 additions and 980 deletions
+263 -237
View File
@@ -1,7 +1,10 @@
package api
import (
"fmt"
"net/http"
"net/url"
"strings"
"home-vue-go/internal/config"
"home-vue-go/internal/database"
@@ -9,271 +12,294 @@ import (
"github.com/gin-gonic/gin"
)
// GetSiteConfig 获取站点配置(含底部年份配置)
func GetSiteConfig(db *database.Database, cfg *config.Config) gin.HandlerFunc {
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) {
ctx := c.Request.Context()
footerCfg, _ := cfg.LoadFooterYear()
visitTimerCfg, _ := cfg.LoadVisitTimer()
siteCfg, err := db.Client.SiteConfig.Get(ctx, 1)
settings, err := db.LoadSiteSettings(c.Request.Context())
if err != nil {
// 如果不存在,返回默认值 + 年份配置
c.JSON(http.StatusOK, gin.H{
"siteName": "个人主页",
"siteURL": "https://example.com",
"siteIcon": "/favicon.ico",
"siteDescription": "一个基于Vue3的个人主页",
"siteKeywords": "个人主页,Vue3",
"userName": "用户",
"profileImageURL": "",
"icpNumber": "暂未填写",
"policeNumber": "暂未填写",
"pageTitle": "个人主页",
"favicon": "/favicon.ico",
"umamiScript": "",
"umamiWebsiteId": "",
"iconLibrary": "//lib.baomitu.com/font-awesome/6.5.0/css/all.min.css",
"fontLibrary": "",
"showVisitTimer": visitTimerCfg.ShowVisitTimer,
"footerYearStart": footerCfg.Start,
"footerYearEnd": footerCfg.End,
})
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"siteName": siteCfg.SiteName,
"siteURL": siteCfg.SiteURL,
"siteIcon": siteCfg.SiteIcon,
"siteDescription": siteCfg.SiteDescription,
"siteKeywords": siteCfg.SiteKeywords,
"userName": siteCfg.UserName,
"profileImageURL": siteCfg.ProfileImageURL,
"icpNumber": siteCfg.IcpNumber,
"policeNumber": siteCfg.PoliceNumber,
"pageTitle": siteCfg.PageTitle,
"favicon": siteCfg.Favicon,
"umamiScript": siteCfg.UmamiScript,
"umamiWebsiteId": siteCfg.UmamiWebsiteID,
"iconLibrary": siteCfg.IconLibrary,
"fontLibrary": siteCfg.FontLibrary,
"showVisitTimer": visitTimerCfg.ShowVisitTimer,
"footerYearStart": footerCfg.Start,
"footerYearEnd": footerCfg.End,
})
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))
}
}
// UpdateSiteConfig 更新站点配置(含底部年份配置)
func UpdateSiteConfig(db *database.Database, cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
var req 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"`
UmamiScript string `json:"umamiScript"`
UmamiWebsiteId string `json:"umamiWebsiteId"`
IconLibrary string `json:"iconLibrary"`
FontLibrary string `json:"fontLibrary"`
ShowVisitTimer *bool `json:"showVisitTimer"`
FooterYearStart string `json:"footerYearStart"`
FooterYearEnd string `json:"footerYearEnd"`
var req updateSiteConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "配置参数格式错误"})
return
}
if err := c.ShouldBindJSON(&req); err != nil {
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
}
ctx := c.Request.Context()
update := db.Client.SiteConfig.UpdateOneID(1)
if req.SiteName != "" {
update.SetSiteName(req.SiteName)
}
if req.SiteURL != "" {
update.SetSiteURL(req.SiteURL)
}
if req.SiteIcon != "" {
update.SetSiteIcon(req.SiteIcon)
}
if req.SiteDescription != "" {
update.SetSiteDescription(req.SiteDescription)
}
if req.SiteKeywords != "" {
update.SetSiteKeywords(req.SiteKeywords)
}
if req.UserName != "" {
update.SetUserName(req.UserName)
}
if req.ProfileImageURL != "" {
update.SetProfileImageURL(req.ProfileImageURL)
}
if req.ICPNumber != "" {
update.SetIcpNumber(req.ICPNumber)
}
if req.PoliceNumber != "" {
update.SetPoliceNumber(req.PoliceNumber)
}
if req.PageTitle != "" {
update.SetPageTitle(req.PageTitle)
}
if req.Favicon != "" {
update.SetFavicon(req.Favicon)
}
if req.UmamiScript != "" {
update.SetUmamiScript(req.UmamiScript)
}
if req.UmamiWebsiteId != "" {
update.SetUmamiWebsiteID(req.UmamiWebsiteId)
}
if req.IconLibrary != "" {
update.SetIconLibrary(req.IconLibrary)
}
if req.FontLibrary != "" {
update.SetFontLibrary(req.FontLibrary)
}
siteCfg, err := update.Save(ctx)
if err != nil {
// 如果不存在,创建新的
create := db.Client.SiteConfig.Create().
SetSiteName(req.SiteName).
SetSiteURL(req.SiteURL).
SetSiteIcon(req.SiteIcon).
SetSiteDescription(req.SiteDescription).
SetSiteKeywords(req.SiteKeywords).
SetUserName(req.UserName)
if req.ProfileImageURL != "" {
create.SetProfileImageURL(req.ProfileImageURL)
}
if req.ICPNumber != "" {
create.SetIcpNumber(req.ICPNumber)
}
if req.PoliceNumber != "" {
create.SetPoliceNumber(req.PoliceNumber)
}
if req.PageTitle != "" {
create.SetPageTitle(req.PageTitle)
}
if req.Favicon != "" {
create.SetFavicon(req.Favicon)
}
if req.UmamiScript != "" {
create.SetUmamiScript(req.UmamiScript)
}
if req.UmamiWebsiteId != "" {
create.SetUmamiWebsiteID(req.UmamiWebsiteId)
}
if req.IconLibrary != "" {
create.SetIconLibrary(req.IconLibrary)
}
if req.FontLibrary != "" {
create.SetFontLibrary(req.FontLibrary)
}
siteCfg, err = create.Save(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
// 保存底部年份配置到独立文件
_ = cfg.SaveFooterYear(&config.FooterYearConfig{
Start: req.FooterYearStart,
End: req.FooterYearEnd,
})
// 保存访问时间显示配置
if req.ShowVisitTimer != nil {
_ = cfg.SaveVisitTimer(&config.VisitTimerConfig{
ShowVisitTimer: *req.ShowVisitTimer,
})
}
// 读取最新的配置用于返回
footerCfg, _ := cfg.LoadFooterYear()
visitTimerCfg, _ := cfg.LoadVisitTimer()
c.JSON(http.StatusOK, gin.H{
"siteName": siteCfg.SiteName,
"siteURL": siteCfg.SiteURL,
"siteIcon": siteCfg.SiteIcon,
"siteDescription": siteCfg.SiteDescription,
"siteKeywords": siteCfg.SiteKeywords,
"userName": siteCfg.UserName,
"profileImageURL": siteCfg.ProfileImageURL,
"icpNumber": siteCfg.IcpNumber,
"policeNumber": siteCfg.PoliceNumber,
"pageTitle": siteCfg.PageTitle,
"favicon": siteCfg.Favicon,
"umamiScript": siteCfg.UmamiScript,
"umamiWebsiteId": siteCfg.UmamiWebsiteID,
"iconLibrary": siteCfg.IconLibrary,
"fontLibrary": siteCfg.FontLibrary,
"showVisitTimer": visitTimerCfg.ShowVisitTimer,
"footerYearStart": footerCfg.Start,
"footerYearEnd": footerCfg.End,
})
}
}
// GetRotatingTexts 获取轮换文本配置
func GetRotatingTexts(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
textsCfg, err := cfg.LoadRotatingTexts()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载轮换文本配置失败: " + err.Error()})
if err := db.SaveSiteSettings(ctx, next); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点配置失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"texts": textsCfg.Texts,
})
c.JSON(http.StatusOK, siteConfigResponse(next, true))
}
}
// UpdateRotatingTexts 更新轮换文本配置
func UpdateRotatingTexts(cfg *config.Config) gin.HandlerFunc {
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": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "轮换文本参数格式错误"})
return
}
// 限制最多8条文本
if len(req.Texts) > 8 {
req.Texts = req.Texts[:8]
}
textsCfg := &config.RotatingTextsConfig{
Texts: req.Texts,
}
if err := cfg.SaveRotatingTexts(textsCfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存轮换文本配置失败: " + err.Error()})
settings, err := db.LoadSiteSettings(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "轮换文本配置已保存",
"texts": textsCfg.Texts,
})
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
}
}