完善后台的图标库引用逻辑和在线图标库的引用
This commit is contained in:
+308
-305
@@ -1,305 +1,308 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
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 地址")
|
||||
}
|
||||
if strings.TrimSpace(link.Icon) != "" && !validIconValue(link.Icon) {
|
||||
return fmt.Errorf("关于链接图标格式不正确")
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+319
-319
@@ -1,319 +1,319 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"home-vue-go/internal/database"
|
||||
"home-vue-go/internal/ent"
|
||||
"home-vue-go/internal/ent/contact"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GetContacts(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contacts, err := db.Client.Contact.Query().Order(contact.BySortOrder(), contact.ByID()).All(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(contacts))
|
||||
for i, contact := range contacts {
|
||||
result[i] = gin.H{
|
||||
"id": contact.ID,
|
||||
"type": contact.Type,
|
||||
"icon": contact.Icon,
|
||||
"url": contact.URL,
|
||||
"qrCode": contact.QrCode,
|
||||
"hoverColor": contact.HoverColor,
|
||||
"sortOrder": contact.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func ReorderContacts(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []int `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[int]bool, len(req.IDs))
|
||||
for _, id := range req.IDs {
|
||||
if id <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含无效ID"})
|
||||
return
|
||||
}
|
||||
if seen[id] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含重复ID"})
|
||||
return
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
existingIDs, err := db.Client.Contact.Query().IDs(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(existingIDs) != len(req.IDs) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表与当前联系方式数量不一致"})
|
||||
return
|
||||
}
|
||||
existing := make(map[int]bool, len(existingIDs))
|
||||
for _, id := range existingIDs {
|
||||
existing[id] = true
|
||||
}
|
||||
for _, id := range req.IDs {
|
||||
if !existing[id] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含不存在的联系方式"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := db.Client.Tx(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range req.IDs {
|
||||
if _, err := tx.Contact.UpdateOneID(id).SetSortOrder((index + 1) * 10).Save(ctx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contacts, err := db.Client.Contact.Query().Order(contact.BySortOrder(), contact.ByID()).All(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(contacts))
|
||||
for i, contact := range contacts {
|
||||
result[i] = gin.H{
|
||||
"id": contact.ID,
|
||||
"type": contact.Type,
|
||||
"icon": contact.Icon,
|
||||
"url": contact.URL,
|
||||
"qrCode": contact.QrCode,
|
||||
"hoverColor": contact.HoverColor,
|
||||
"sortOrder": contact.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateContact(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Icon string `json:"icon" binding:"required"`
|
||||
URL string `json:"url"`
|
||||
QrCode string `json:"qrCode"`
|
||||
HoverColor string `json:"hoverColor"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Type = strings.TrimSpace(req.Type)
|
||||
req.Icon = strings.TrimSpace(req.Icon)
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
req.QrCode = strings.TrimSpace(req.QrCode)
|
||||
req.HoverColor = strings.TrimSpace(req.HoverColor)
|
||||
if req.Type == "" || req.Icon == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "类型和图标不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL != "" && !validContactURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式URL必须是有效的http/https/mailto/tel地址"})
|
||||
return
|
||||
}
|
||||
if req.QrCode != "" && !validQRCode(req.QrCode) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "二维码必须使用本地上传路径或安全的http/https图片地址"})
|
||||
return
|
||||
}
|
||||
if req.URL != "" && req.QrCode != "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式不能同时设置链接和二维码"})
|
||||
return
|
||||
}
|
||||
if req.URL == "" && req.QrCode == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "链接或二维码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
create := db.Client.Contact.Create().
|
||||
SetType(req.Type).
|
||||
SetIcon(req.Icon).
|
||||
SetSortOrder(req.SortOrder)
|
||||
|
||||
if req.URL != "" {
|
||||
create.SetURL(req.URL)
|
||||
}
|
||||
if req.QrCode != "" {
|
||||
create.SetQrCode(req.QrCode)
|
||||
}
|
||||
if req.HoverColor != "" {
|
||||
create.SetHoverColor(req.HoverColor)
|
||||
}
|
||||
|
||||
contact, err := create.Save(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": contact.ID,
|
||||
"type": contact.Type,
|
||||
"icon": contact.Icon,
|
||||
"url": contact.URL,
|
||||
"qrCode": contact.QrCode,
|
||||
"hoverColor": contact.HoverColor,
|
||||
"sortOrder": contact.SortOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateContact(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Icon string `json:"icon" binding:"required"`
|
||||
URL string `json:"url"`
|
||||
QrCode string `json:"qrCode"`
|
||||
HoverColor string `json:"hoverColor"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Type = strings.TrimSpace(req.Type)
|
||||
req.Icon = strings.TrimSpace(req.Icon)
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
req.QrCode = strings.TrimSpace(req.QrCode)
|
||||
req.HoverColor = strings.TrimSpace(req.HoverColor)
|
||||
if req.Type == "" || req.Icon == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "类型和图标不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL != "" && !validContactURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式URL必须是有效的http/https/mailto/tel地址"})
|
||||
return
|
||||
}
|
||||
if req.QrCode != "" && !validQRCode(req.QrCode) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "二维码必须使用本地上传路径或安全的http/https图片地址"})
|
||||
return
|
||||
}
|
||||
if req.URL != "" && req.QrCode != "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式不能同时设置链接和二维码"})
|
||||
return
|
||||
}
|
||||
if req.URL == "" && req.QrCode == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "链接或二维码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
update := db.Client.Contact.UpdateOneID(id)
|
||||
update.SetType(req.Type)
|
||||
update.SetIcon(req.Icon)
|
||||
if req.URL != "" {
|
||||
update.SetURL(req.URL)
|
||||
} else {
|
||||
update.ClearURL()
|
||||
}
|
||||
if req.QrCode != "" {
|
||||
update.SetQrCode(req.QrCode)
|
||||
} else {
|
||||
update.ClearQrCode()
|
||||
}
|
||||
if req.HoverColor != "" {
|
||||
update.SetHoverColor(req.HoverColor)
|
||||
} else {
|
||||
update.ClearHoverColor()
|
||||
}
|
||||
update.SetSortOrder(req.SortOrder)
|
||||
|
||||
contact, err := update.Save(ctx)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "联系方式不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": contact.ID,
|
||||
"type": contact.Type,
|
||||
"icon": contact.Icon,
|
||||
"url": contact.URL,
|
||||
"qrCode": contact.QrCode,
|
||||
"hoverColor": contact.HoverColor,
|
||||
"sortOrder": contact.SortOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteContact(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := db.Client.Contact.DeleteOneID(id).Exec(ctx); err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "联系方式不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
}
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"home-vue-go/internal/database"
|
||||
"home-vue-go/internal/ent"
|
||||
"home-vue-go/internal/ent/contact"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GetContacts(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contacts, err := db.Client.Contact.Query().Order(contact.BySortOrder(), contact.ByID()).All(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(contacts))
|
||||
for i, contact := range contacts {
|
||||
result[i] = gin.H{
|
||||
"id": contact.ID,
|
||||
"type": contact.Type,
|
||||
"icon": contact.Icon,
|
||||
"url": contact.URL,
|
||||
"qrCode": contact.QrCode,
|
||||
"hoverColor": contact.HoverColor,
|
||||
"sortOrder": contact.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func ReorderContacts(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []int `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[int]bool, len(req.IDs))
|
||||
for _, id := range req.IDs {
|
||||
if id <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含无效ID"})
|
||||
return
|
||||
}
|
||||
if seen[id] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含重复ID"})
|
||||
return
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
existingIDs, err := db.Client.Contact.Query().IDs(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(existingIDs) != len(req.IDs) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表与当前联系方式数量不一致"})
|
||||
return
|
||||
}
|
||||
existing := make(map[int]bool, len(existingIDs))
|
||||
for _, id := range existingIDs {
|
||||
existing[id] = true
|
||||
}
|
||||
for _, id := range req.IDs {
|
||||
if !existing[id] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含不存在的联系方式"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := db.Client.Tx(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range req.IDs {
|
||||
if _, err := tx.Contact.UpdateOneID(id).SetSortOrder((index + 1) * 10).Save(ctx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contacts, err := db.Client.Contact.Query().Order(contact.BySortOrder(), contact.ByID()).All(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(contacts))
|
||||
for i, contact := range contacts {
|
||||
result[i] = gin.H{
|
||||
"id": contact.ID,
|
||||
"type": contact.Type,
|
||||
"icon": contact.Icon,
|
||||
"url": contact.URL,
|
||||
"qrCode": contact.QrCode,
|
||||
"hoverColor": contact.HoverColor,
|
||||
"sortOrder": contact.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateContact(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Icon string `json:"icon" binding:"required"`
|
||||
URL string `json:"url"`
|
||||
QrCode string `json:"qrCode"`
|
||||
HoverColor string `json:"hoverColor"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Type = strings.TrimSpace(req.Type)
|
||||
req.Icon = strings.TrimSpace(req.Icon)
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
req.QrCode = strings.TrimSpace(req.QrCode)
|
||||
req.HoverColor = strings.TrimSpace(req.HoverColor)
|
||||
if req.Type == "" || !validIconValue(req.Icon) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "类型和图标不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL != "" && !validContactURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式URL必须是有效的http/https/mailto/tel地址"})
|
||||
return
|
||||
}
|
||||
if req.QrCode != "" && !validQRCode(req.QrCode) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "二维码必须使用本地上传路径或安全的http/https图片地址"})
|
||||
return
|
||||
}
|
||||
if req.URL != "" && req.QrCode != "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式不能同时设置链接和二维码"})
|
||||
return
|
||||
}
|
||||
if req.URL == "" && req.QrCode == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "链接或二维码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
create := db.Client.Contact.Create().
|
||||
SetType(req.Type).
|
||||
SetIcon(req.Icon).
|
||||
SetSortOrder(req.SortOrder)
|
||||
|
||||
if req.URL != "" {
|
||||
create.SetURL(req.URL)
|
||||
}
|
||||
if req.QrCode != "" {
|
||||
create.SetQrCode(req.QrCode)
|
||||
}
|
||||
if req.HoverColor != "" {
|
||||
create.SetHoverColor(req.HoverColor)
|
||||
}
|
||||
|
||||
contact, err := create.Save(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": contact.ID,
|
||||
"type": contact.Type,
|
||||
"icon": contact.Icon,
|
||||
"url": contact.URL,
|
||||
"qrCode": contact.QrCode,
|
||||
"hoverColor": contact.HoverColor,
|
||||
"sortOrder": contact.SortOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateContact(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Icon string `json:"icon" binding:"required"`
|
||||
URL string `json:"url"`
|
||||
QrCode string `json:"qrCode"`
|
||||
HoverColor string `json:"hoverColor"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Type = strings.TrimSpace(req.Type)
|
||||
req.Icon = strings.TrimSpace(req.Icon)
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
req.QrCode = strings.TrimSpace(req.QrCode)
|
||||
req.HoverColor = strings.TrimSpace(req.HoverColor)
|
||||
if req.Type == "" || !validIconValue(req.Icon) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "类型和图标不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL != "" && !validContactURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式URL必须是有效的http/https/mailto/tel地址"})
|
||||
return
|
||||
}
|
||||
if req.QrCode != "" && !validQRCode(req.QrCode) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "二维码必须使用本地上传路径或安全的http/https图片地址"})
|
||||
return
|
||||
}
|
||||
if req.URL != "" && req.QrCode != "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式不能同时设置链接和二维码"})
|
||||
return
|
||||
}
|
||||
if req.URL == "" && req.QrCode == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "链接或二维码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
update := db.Client.Contact.UpdateOneID(id)
|
||||
update.SetType(req.Type)
|
||||
update.SetIcon(req.Icon)
|
||||
if req.URL != "" {
|
||||
update.SetURL(req.URL)
|
||||
} else {
|
||||
update.ClearURL()
|
||||
}
|
||||
if req.QrCode != "" {
|
||||
update.SetQrCode(req.QrCode)
|
||||
} else {
|
||||
update.ClearQrCode()
|
||||
}
|
||||
if req.HoverColor != "" {
|
||||
update.SetHoverColor(req.HoverColor)
|
||||
} else {
|
||||
update.ClearHoverColor()
|
||||
}
|
||||
update.SetSortOrder(req.SortOrder)
|
||||
|
||||
contact, err := update.Save(ctx)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "联系方式不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": contact.ID,
|
||||
"type": contact.Type,
|
||||
"icon": contact.Icon,
|
||||
"url": contact.URL,
|
||||
"qrCode": contact.QrCode,
|
||||
"hoverColor": contact.HoverColor,
|
||||
"sortOrder": contact.SortOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteContact(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := db.Client.Contact.DeleteOneID(id).Exec(ctx); err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "联系方式不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
}
|
||||
|
||||
+242
-242
@@ -1,242 +1,242 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"home-vue-go/internal/database"
|
||||
"home-vue-go/internal/ent"
|
||||
"home-vue-go/internal/ent/site"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GetSites(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
sites, err := db.Client.Site.Query().Order(site.BySortOrder(), site.ByID()).All(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(sites))
|
||||
for i, site := range sites {
|
||||
result[i] = gin.H{
|
||||
"id": site.ID,
|
||||
"name": site.Name,
|
||||
"url": site.URL,
|
||||
"icon": site.Icon,
|
||||
"sortOrder": site.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func ReorderSites(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []int `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[int]bool, len(req.IDs))
|
||||
for _, id := range req.IDs {
|
||||
if id <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含无效ID"})
|
||||
return
|
||||
}
|
||||
if seen[id] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含重复ID"})
|
||||
return
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
existingIDs, err := db.Client.Site.Query().IDs(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(existingIDs) != len(req.IDs) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表与当前站点数量不一致"})
|
||||
return
|
||||
}
|
||||
existing := make(map[int]bool, len(existingIDs))
|
||||
for _, id := range existingIDs {
|
||||
existing[id] = true
|
||||
}
|
||||
for _, id := range req.IDs {
|
||||
if !existing[id] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含不存在的站点"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := db.Client.Tx(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range req.IDs {
|
||||
if _, err := tx.Site.UpdateOneID(id).SetSortOrder((index + 1) * 10).Save(ctx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
sites, err := db.Client.Site.Query().Order(site.BySortOrder(), site.ByID()).All(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(sites))
|
||||
for i, site := range sites {
|
||||
result[i] = gin.H{
|
||||
"id": site.ID,
|
||||
"name": site.Name,
|
||||
"url": site.URL,
|
||||
"icon": site.Icon,
|
||||
"sortOrder": site.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSite(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
Icon string `json:"icon" binding:"required"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Name, req.URL, req.Icon = strings.TrimSpace(req.Name), strings.TrimSpace(req.URL), strings.TrimSpace(req.Icon)
|
||||
if req.Name == "" || req.URL == "" || req.Icon == "" || !validHTTPURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称、图标不能为空,URL必须是有效的http/https地址"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
site, err := db.Client.Site.Create().
|
||||
SetName(req.Name).
|
||||
SetURL(req.URL).
|
||||
SetIcon(req.Icon).
|
||||
SetSortOrder(req.SortOrder).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": site.ID,
|
||||
"name": site.Name,
|
||||
"url": site.URL,
|
||||
"icon": site.Icon,
|
||||
"sortOrder": site.SortOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateSite(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Icon string `json:"icon"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Name, req.URL, req.Icon = strings.TrimSpace(req.Name), strings.TrimSpace(req.URL), strings.TrimSpace(req.Icon)
|
||||
if req.Name == "" || req.URL == "" || req.Icon == "" || !validHTTPURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称、图标不能为空,URL必须是有效的http/https地址"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
update := db.Client.Site.UpdateOneID(id)
|
||||
update.SetName(req.Name)
|
||||
update.SetURL(req.URL)
|
||||
update.SetIcon(req.Icon)
|
||||
update.SetSortOrder(req.SortOrder)
|
||||
|
||||
site, err := update.Save(ctx)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": site.ID,
|
||||
"name": site.Name,
|
||||
"url": site.URL,
|
||||
"icon": site.Icon,
|
||||
"sortOrder": site.SortOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSite(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := db.Client.Site.DeleteOneID(id).Exec(ctx); err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
}
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"home-vue-go/internal/database"
|
||||
"home-vue-go/internal/ent"
|
||||
"home-vue-go/internal/ent/site"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GetSites(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
sites, err := db.Client.Site.Query().Order(site.BySortOrder(), site.ByID()).All(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(sites))
|
||||
for i, site := range sites {
|
||||
result[i] = gin.H{
|
||||
"id": site.ID,
|
||||
"name": site.Name,
|
||||
"url": site.URL,
|
||||
"icon": site.Icon,
|
||||
"sortOrder": site.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func ReorderSites(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []int `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[int]bool, len(req.IDs))
|
||||
for _, id := range req.IDs {
|
||||
if id <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含无效ID"})
|
||||
return
|
||||
}
|
||||
if seen[id] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含重复ID"})
|
||||
return
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
existingIDs, err := db.Client.Site.Query().IDs(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(existingIDs) != len(req.IDs) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表与当前站点数量不一致"})
|
||||
return
|
||||
}
|
||||
existing := make(map[int]bool, len(existingIDs))
|
||||
for _, id := range existingIDs {
|
||||
existing[id] = true
|
||||
}
|
||||
for _, id := range req.IDs {
|
||||
if !existing[id] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含不存在的站点"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := db.Client.Tx(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range req.IDs {
|
||||
if _, err := tx.Site.UpdateOneID(id).SetSortOrder((index + 1) * 10).Save(ctx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
sites, err := db.Client.Site.Query().Order(site.BySortOrder(), site.ByID()).All(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(sites))
|
||||
for i, site := range sites {
|
||||
result[i] = gin.H{
|
||||
"id": site.ID,
|
||||
"name": site.Name,
|
||||
"url": site.URL,
|
||||
"icon": site.Icon,
|
||||
"sortOrder": site.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSite(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
Icon string `json:"icon" binding:"required"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Name, req.URL, req.Icon = strings.TrimSpace(req.Name), strings.TrimSpace(req.URL), strings.TrimSpace(req.Icon)
|
||||
if req.Name == "" || req.URL == "" || !validIconValue(req.Icon) || !validHTTPURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称、图标不能为空,URL必须是有效的http/https地址"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
site, err := db.Client.Site.Create().
|
||||
SetName(req.Name).
|
||||
SetURL(req.URL).
|
||||
SetIcon(req.Icon).
|
||||
SetSortOrder(req.SortOrder).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": site.ID,
|
||||
"name": site.Name,
|
||||
"url": site.URL,
|
||||
"icon": site.Icon,
|
||||
"sortOrder": site.SortOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateSite(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Icon string `json:"icon"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Name, req.URL, req.Icon = strings.TrimSpace(req.Name), strings.TrimSpace(req.URL), strings.TrimSpace(req.Icon)
|
||||
if req.Name == "" || req.URL == "" || !validIconValue(req.Icon) || !validHTTPURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称、图标不能为空,URL必须是有效的http/https地址"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
update := db.Client.Site.UpdateOneID(id)
|
||||
update.SetName(req.Name)
|
||||
update.SetURL(req.URL)
|
||||
update.SetIcon(req.Icon)
|
||||
update.SetSortOrder(req.SortOrder)
|
||||
|
||||
site, err := update.Save(ctx)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": site.ID,
|
||||
"name": site.Name,
|
||||
"url": site.URL,
|
||||
"icon": site.Icon,
|
||||
"sortOrder": site.SortOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSite(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := db.Client.Site.DeleteOneID(id).Exec(ctx); err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,17 @@ package api
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const maxIconValueLength = 160
|
||||
|
||||
var (
|
||||
legacyIconPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+(?:\s+[A-Za-z0-9_-]+)*$`)
|
||||
iconifyValuePattern = regexp.MustCompile(`^iconify:[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$`)
|
||||
)
|
||||
|
||||
func validHTTPURL(value string) bool {
|
||||
parsed, err := url.ParseRequestURI(strings.TrimSpace(value))
|
||||
return err == nil && parsed.Host != "" && (parsed.Scheme == "http" || parsed.Scheme == "https")
|
||||
@@ -30,3 +38,14 @@ func validQRCode(value string) bool {
|
||||
}
|
||||
return validHTTPURL(value)
|
||||
}
|
||||
|
||||
func validIconValue(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || len(value) > maxIconValueLength {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(value, "iconify:") {
|
||||
return iconifyValuePattern.MatchString(value)
|
||||
}
|
||||
return legacyIconPattern.MatchString(value)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"home-vue-go/internal/config"
|
||||
)
|
||||
|
||||
func TestURLValidation(t *testing.T) {
|
||||
for _, value := range []string{"https://example.com", "http://localhost:8080"} {
|
||||
@@ -30,6 +35,49 @@ func TestURLValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIconValueValidation(t *testing.T) {
|
||||
for _, value := range []string{
|
||||
"fas fa-home",
|
||||
"fab fa-github",
|
||||
"iconify:mdi:home",
|
||||
"iconify:material-symbols:add-home-outline",
|
||||
} {
|
||||
if !validIconValue(value) {
|
||||
t.Errorf("expected valid icon value: %s", value)
|
||||
}
|
||||
}
|
||||
for _, value := range []string{
|
||||
"",
|
||||
"iconify:MDI:home",
|
||||
"iconify:mdi:",
|
||||
"iconify:mdi:home/../../x",
|
||||
"fas fa-home\"><script>",
|
||||
strings.Repeat("a", maxIconValueLength+1),
|
||||
} {
|
||||
if validIconValue(value) {
|
||||
t.Errorf("expected invalid icon value: %s", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAboutLinkIconValidation(t *testing.T) {
|
||||
settings := config.DefaultSiteSettings()
|
||||
settings.AboutLinks[0].Icon = ""
|
||||
if err := validateSiteSettings(settings); err != nil {
|
||||
t.Fatalf("expected empty about link icon to use the frontend fallback: %v", err)
|
||||
}
|
||||
|
||||
settings.AboutLinks[0].Icon = "iconify:tabler:brand-github"
|
||||
if err := validateSiteSettings(settings); err != nil {
|
||||
t.Fatalf("expected valid Iconify about link: %v", err)
|
||||
}
|
||||
|
||||
settings.AboutLinks[0].Icon = "<script>"
|
||||
if err := validateSiteSettings(settings); err == nil {
|
||||
t.Fatal("expected invalid about link icon to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPercentageCountsSumsToOneHundred(t *testing.T) {
|
||||
values := percentageCounts(map[string]int{"direct": 1, "search": 1, "other": 1}, 3)
|
||||
total := 0
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
"home-vue-go/internal/ent"
|
||||
"home-vue-go/internal/ent/migrate"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const siteConfigPayloadColumn = "config_json"
|
||||
@@ -28,7 +28,7 @@ type Database struct {
|
||||
}
|
||||
|
||||
func Init(dbPath string, cfg *config.Config) (*Database, error) {
|
||||
db, err := sql.Open("sqlite3", dbPath+"?_fk=1")
|
||||
db, err := sql.Open("sqlite", dbPath+"?_pragma=foreign_keys(1)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"home-vue-go/internal/config"
|
||||
@@ -25,9 +24,6 @@ func TestSiteSettingsMigrationAndBooleanPersistence(t *testing.T) {
|
||||
cfg := config.New(dataDir)
|
||||
db, err := Init(cfg.DatabasePath, cfg)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "CGO_ENABLED=0") {
|
||||
t.Skip("go-sqlite3 requires cgo for the database integration test")
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package ent
|
||||
|
||||
//go:generate go run -mod=mod entgo.io/ent/cmd/ent@v0.14.5 generate ./schema
|
||||
//go:generate go run -mod=readonly entgo.io/ent/cmd/ent generate ./schema
|
||||
|
||||
Reference in New Issue
Block a user