mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
add: sitemap
This commit is contained in:
@@ -74,6 +74,14 @@ const (
|
||||
ConfigKeyWechatSearchImage = "wechat_search_image"
|
||||
ConfigKeyTelegramQrImage = "telegram_qr_image"
|
||||
ConfigKeyQrCodeStyle = "qr_code_style"
|
||||
|
||||
// Sitemap配置
|
||||
ConfigKeySitemapConfig = "sitemap_config"
|
||||
ConfigKeySitemapLastGenerateTime = "sitemap_last_generate_time"
|
||||
ConfigKeySitemapAutoGenerateEnabled = "sitemap_auto_generate_enabled"
|
||||
|
||||
// 网站URL配置
|
||||
ConfigKeyWebsiteURL = "website_url"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型常量
|
||||
|
||||
@@ -12,6 +12,7 @@ type BaseRepository[T any] interface {
|
||||
Update(entity *T) error
|
||||
Delete(id uint) error
|
||||
FindWithPagination(page, limit int) ([]T, int64, error)
|
||||
GetDB() *gorm.DB
|
||||
}
|
||||
|
||||
// BaseRepositoryImpl 基础Repository实现
|
||||
|
||||
411
handlers/sitemap_handler.go
Normal file
411
handlers/sitemap_handler.go
Normal file
@@ -0,0 +1,411 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/scheduler"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
resourceRepo repo.ResourceRepository
|
||||
systemConfigRepo repo.SystemConfigRepository
|
||||
hotDramaRepo repo.HotDramaRepository
|
||||
readyResourceRepo repo.ReadyResourceRepository
|
||||
panRepo repo.PanRepository
|
||||
cksRepo repo.CksRepository
|
||||
tagRepo repo.TagRepository
|
||||
categoryRepo repo.CategoryRepository
|
||||
)
|
||||
|
||||
// SetSitemapDependencies 注册Sitemap处理器依赖
|
||||
func SetSitemapDependencies(
|
||||
resourceRepository repo.ResourceRepository,
|
||||
systemConfigRepository repo.SystemConfigRepository,
|
||||
hotDramaRepository repo.HotDramaRepository,
|
||||
readyResourceRepository repo.ReadyResourceRepository,
|
||||
panRepository repo.PanRepository,
|
||||
cksRepository repo.CksRepository,
|
||||
tagRepository repo.TagRepository,
|
||||
categoryRepository repo.CategoryRepository,
|
||||
) {
|
||||
resourceRepo = resourceRepository
|
||||
systemConfigRepo = systemConfigRepository
|
||||
hotDramaRepo = hotDramaRepository
|
||||
readyResourceRepo = readyResourceRepository
|
||||
panRepo = panRepository
|
||||
cksRepo = cksRepository
|
||||
tagRepo = tagRepository
|
||||
categoryRepo = categoryRepository
|
||||
}
|
||||
|
||||
|
||||
const SITEMAP_MAX_URLS = 50000 // 每个sitemap最多5万个URL
|
||||
|
||||
// SitemapIndex sitemap索引结构
|
||||
type SitemapIndex struct {
|
||||
XMLName xml.Name `xml:"sitemapindex"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
Sitemaps []Sitemap `xml:"sitemap"`
|
||||
}
|
||||
|
||||
// Sitemap 单个sitemap信息
|
||||
type Sitemap struct {
|
||||
Loc string `xml:"loc"`
|
||||
LastMod string `xml:"lastmod"`
|
||||
}
|
||||
|
||||
// UrlSet sitemap内容
|
||||
type UrlSet struct {
|
||||
XMLName xml.Name `xml:"urlset"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
URLs []Url `xml:"url"`
|
||||
}
|
||||
|
||||
// Url 单个URL信息
|
||||
type Url struct {
|
||||
Loc string `xml:"loc"`
|
||||
LastMod string `xml:"lastmod"`
|
||||
ChangeFreq string `xml:"changefreq"`
|
||||
Priority float64 `xml:"priority"`
|
||||
}
|
||||
|
||||
// SitemapConfig sitemap配置
|
||||
type SitemapConfig struct {
|
||||
AutoGenerate bool `json:"auto_generate"`
|
||||
LastGenerate time.Time `json:"last_generate"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
// GetSitemapConfig 获取sitemap配置
|
||||
func GetSitemapConfig(c *gin.Context) {
|
||||
// 从全局调度器获取配置
|
||||
enabled, err := scheduler.GetGlobalScheduler(
|
||||
hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo,
|
||||
panRepo, cksRepo, tagRepo, categoryRepo,
|
||||
).GetSitemapConfig()
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
// 如果获取失败,尝试从配置表中获取
|
||||
configStr, err := systemConfigRepo.GetConfigValue(entity.ConfigKeySitemapAutoGenerateEnabled)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
ErrorResponse(c, "获取配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
enabled = configStr == "1" || configStr == "true"
|
||||
}
|
||||
|
||||
// 获取最后生成时间(从配置中获取)
|
||||
configStr, err := systemConfigRepo.GetConfigValue(entity.ConfigKeySitemapLastGenerateTime)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
// 如果获取失败,只返回启用状态
|
||||
config := SitemapConfig{
|
||||
AutoGenerate: enabled,
|
||||
LastGenerate: time.Time{}, // 空时间
|
||||
LastUpdate: time.Now(),
|
||||
}
|
||||
SuccessResponse(c, config)
|
||||
return
|
||||
}
|
||||
|
||||
var lastGenerateTime time.Time
|
||||
if configStr != "" {
|
||||
lastGenerateTime, _ = time.Parse("2006-01-02 15:04:05", configStr)
|
||||
}
|
||||
|
||||
config := SitemapConfig{
|
||||
AutoGenerate: enabled,
|
||||
LastGenerate: lastGenerateTime,
|
||||
LastUpdate: time.Now(),
|
||||
}
|
||||
|
||||
SuccessResponse(c, config)
|
||||
}
|
||||
|
||||
// UpdateSitemapConfig 更新sitemap配置
|
||||
func UpdateSitemapConfig(c *gin.Context) {
|
||||
var config SitemapConfig
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
ErrorResponse(c, "参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新调度器配置
|
||||
if err := scheduler.GetGlobalScheduler(
|
||||
hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo,
|
||||
panRepo, cksRepo, tagRepo, categoryRepo,
|
||||
).UpdateSitemapConfig(config.AutoGenerate); err != nil {
|
||||
ErrorResponse(c, "更新调度器配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存自动生成功能状态
|
||||
autoGenerateStr := "0"
|
||||
if config.AutoGenerate {
|
||||
autoGenerateStr = "1"
|
||||
}
|
||||
autoGenerateConfig := entity.SystemConfig{
|
||||
Key: entity.ConfigKeySitemapAutoGenerateEnabled,
|
||||
Value: autoGenerateStr,
|
||||
Type: "bool",
|
||||
}
|
||||
|
||||
// 保存最后生成时间
|
||||
lastGenerateStr := config.LastGenerate.Format("2006-01-02 15:04:05")
|
||||
lastGenerateConfig := entity.SystemConfig{
|
||||
Key: entity.ConfigKeySitemapLastGenerateTime,
|
||||
Value: lastGenerateStr,
|
||||
Type: "string",
|
||||
}
|
||||
|
||||
configs := []entity.SystemConfig{autoGenerateConfig, lastGenerateConfig}
|
||||
if err := systemConfigRepo.UpsertConfigs(configs); err != nil {
|
||||
ErrorResponse(c, "保存配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 根据配置启动或停止调度器
|
||||
if config.AutoGenerate {
|
||||
scheduler.GetGlobalScheduler(
|
||||
hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo,
|
||||
panRepo, cksRepo, tagRepo, categoryRepo,
|
||||
).StartSitemapScheduler()
|
||||
} else {
|
||||
scheduler.GetGlobalScheduler(
|
||||
hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo,
|
||||
panRepo, cksRepo, tagRepo, categoryRepo,
|
||||
).StopSitemapScheduler()
|
||||
}
|
||||
|
||||
SuccessResponse(c, config)
|
||||
}
|
||||
|
||||
// GenerateSitemap 手动生成sitemap
|
||||
func GenerateSitemap(c *gin.Context) {
|
||||
// 获取资源总数
|
||||
var total int64
|
||||
if err := resourceRepo.GetDB().Model(&entity.Resource{}).Count(&total).Error; err != nil {
|
||||
ErrorResponse(c, "获取资源总数失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
totalPages := int((total + SITEMAP_MAX_URLS - 1) / SITEMAP_MAX_URLS)
|
||||
|
||||
// 获取全局调度器并立即执行sitemap生成
|
||||
globalScheduler := scheduler.GetGlobalScheduler(
|
||||
hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo,
|
||||
panRepo, cksRepo, tagRepo, categoryRepo,
|
||||
)
|
||||
|
||||
// 手动触发sitemap生成
|
||||
globalScheduler.TriggerSitemapGeneration()
|
||||
|
||||
// 记录最后生成时间为当前时间
|
||||
lastGenerateStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
lastGenerateConfig := entity.SystemConfig{
|
||||
Key: entity.ConfigKeySitemapLastGenerateTime,
|
||||
Value: lastGenerateStr,
|
||||
Type: "string",
|
||||
}
|
||||
|
||||
if err := systemConfigRepo.UpsertConfigs([]entity.SystemConfig{lastGenerateConfig}); err != nil {
|
||||
ErrorResponse(c, "更新最后生成时间失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"total_resources": total,
|
||||
"total_pages": totalPages,
|
||||
"status": "started",
|
||||
"message": fmt.Sprintf("开始生成 %d 个sitemap文件", totalPages),
|
||||
}
|
||||
|
||||
SuccessResponse(c, result)
|
||||
}
|
||||
|
||||
// GetSitemapStatus 获取sitemap生成状态
|
||||
func GetSitemapStatus(c *gin.Context) {
|
||||
// 获取资源总数
|
||||
var total int64
|
||||
if err := resourceRepo.GetDB().Model(&entity.Resource{}).Count(&total).Error; err != nil {
|
||||
ErrorResponse(c, "获取资源总数失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 计算需要生成的sitemap文件数量
|
||||
totalPages := int((total + SITEMAP_MAX_URLS - 1) / SITEMAP_MAX_URLS)
|
||||
|
||||
// 获取最后生成时间
|
||||
lastGenerateStr, err := systemConfigRepo.GetConfigValue(entity.ConfigKeySitemapLastGenerateTime)
|
||||
if err != nil {
|
||||
// 如果没有记录,使用当前时间
|
||||
lastGenerateStr = time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
lastGenerate, err := time.Parse("2006-01-02 15:04:05", lastGenerateStr)
|
||||
if err != nil {
|
||||
lastGenerate = time.Now()
|
||||
}
|
||||
|
||||
// 检查调度器是否运行
|
||||
isRunning := scheduler.GetGlobalScheduler(
|
||||
hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo,
|
||||
panRepo, cksRepo, tagRepo, categoryRepo,
|
||||
).IsSitemapSchedulerRunning()
|
||||
|
||||
// 获取自动生成功能状态
|
||||
autoGenerateEnabled, err := scheduler.GetGlobalScheduler(
|
||||
hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo,
|
||||
panRepo, cksRepo, tagRepo, categoryRepo,
|
||||
).GetSitemapConfig()
|
||||
if err != nil {
|
||||
// 如果调度器获取失败,从配置中获取
|
||||
configStr, err := systemConfigRepo.GetConfigValue(entity.ConfigKeySitemapAutoGenerateEnabled)
|
||||
if err != nil {
|
||||
autoGenerateEnabled = false
|
||||
} else {
|
||||
autoGenerateEnabled = configStr == "1" || configStr == "true"
|
||||
}
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"total_resources": total,
|
||||
"total_pages": totalPages,
|
||||
"last_generate": lastGenerate.Format("2006-01-02 15:04:05"),
|
||||
"status": "ready",
|
||||
"is_running": isRunning,
|
||||
"auto_generate": autoGenerateEnabled,
|
||||
}
|
||||
|
||||
SuccessResponse(c, result)
|
||||
}
|
||||
|
||||
// SitemapIndexHandler sitemap索引文件处理器
|
||||
func SitemapIndexHandler(c *gin.Context) {
|
||||
// 获取资源总数
|
||||
var total int64
|
||||
if err := resourceRepo.GetDB().Model(&entity.Resource{}).Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取资源总数失败"})
|
||||
return
|
||||
}
|
||||
|
||||
totalPages := int((total + SITEMAP_MAX_URLS - 1) / SITEMAP_MAX_URLS)
|
||||
|
||||
// 构建主机URL
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
||||
scheme = "https"
|
||||
}
|
||||
host := c.Request.Host
|
||||
if host == "" {
|
||||
host = "localhost:8080" // 默认值
|
||||
}
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
|
||||
// 创建sitemap列表 - 现在文件保存在data/sitemap目录,通过/file/sitemap/路径访问
|
||||
var sitemaps []Sitemap
|
||||
for i := 0; i < totalPages; i++ {
|
||||
sitemapURL := fmt.Sprintf("%s/file/sitemap/sitemap-%d.xml", baseURL, i)
|
||||
sitemaps = append(sitemaps, Sitemap{
|
||||
Loc: sitemapURL,
|
||||
LastMod: time.Now().Format("2006-01-02"),
|
||||
})
|
||||
}
|
||||
|
||||
sitemapIndex := SitemapIndex{
|
||||
XMLNS: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
||||
Sitemaps: sitemaps,
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/xml")
|
||||
c.XML(http.StatusOK, sitemapIndex)
|
||||
}
|
||||
|
||||
// SitemapPageHandler sitemap页面处理器
|
||||
func SitemapPageHandler(c *gin.Context) {
|
||||
pageStr := c.Param("page")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的页面参数"})
|
||||
return
|
||||
}
|
||||
|
||||
offset := page * SITEMAP_MAX_URLS
|
||||
limit := SITEMAP_MAX_URLS
|
||||
|
||||
var resources []entity.Resource
|
||||
if err := resourceRepo.GetDB().Offset(offset).Limit(limit).Find(&resources).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取资源数据失败"})
|
||||
return
|
||||
}
|
||||
|
||||
var urls []Url
|
||||
for _, resource := range resources {
|
||||
lastMod := resource.UpdatedAt
|
||||
if resource.CreatedAt.After(lastMod) {
|
||||
lastMod = resource.CreatedAt
|
||||
}
|
||||
|
||||
urls = append(urls, Url{
|
||||
Loc: fmt.Sprintf("/r/%s", resource.Key),
|
||||
LastMod: lastMod.Format("2006-01-01"), // 只保留日期部分
|
||||
ChangeFreq: "weekly",
|
||||
Priority: 0.8,
|
||||
})
|
||||
}
|
||||
|
||||
urlSet := UrlSet{
|
||||
XMLNS: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
||||
URLs: urls,
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/xml")
|
||||
c.XML(http.StatusOK, urlSet)
|
||||
}
|
||||
|
||||
// 手动生成完整sitemap文件
|
||||
func GenerateFullSitemap(c *gin.Context) {
|
||||
// 获取资源总数
|
||||
var total int64
|
||||
if err := resourceRepo.GetDB().Model(&entity.Resource{}).Count(&total).Error; err != nil {
|
||||
ErrorResponse(c, "获取资源总数失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取全局调度器并立即执行sitemap生成
|
||||
globalScheduler := scheduler.GetGlobalScheduler(
|
||||
hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo,
|
||||
panRepo, cksRepo, tagRepo, categoryRepo,
|
||||
)
|
||||
|
||||
// 手动触发sitemap生成
|
||||
globalScheduler.TriggerSitemapGeneration()
|
||||
|
||||
// 记录最后生成时间为当前时间
|
||||
lastGenerateStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
lastGenerateConfig := entity.SystemConfig{
|
||||
Key: entity.ConfigKeySitemapLastGenerateTime,
|
||||
Value: lastGenerateStr,
|
||||
Type: "string",
|
||||
}
|
||||
|
||||
if err := systemConfigRepo.UpsertConfigs([]entity.SystemConfig{lastGenerateConfig}); err != nil {
|
||||
ErrorResponse(c, "更新最后生成时间失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"message": "Sitemap生成任务已启动",
|
||||
"total_resources": total,
|
||||
"status": "processing",
|
||||
"estimated_time": fmt.Sprintf("%d秒", total/1000), // 估算时间
|
||||
}
|
||||
|
||||
SuccessResponse(c, result)
|
||||
}
|
||||
56
main.go
56
main.go
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
@@ -159,6 +160,18 @@ func main() {
|
||||
// 将Repository管理器注入到services中
|
||||
services.SetRepositoryManager(repoManager)
|
||||
|
||||
// 设置Sitemap处理器依赖
|
||||
handlers.SetSitemapDependencies(
|
||||
repoManager.ResourceRepository,
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.HotDramaRepository,
|
||||
repoManager.ReadyResourceRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
|
||||
// 设置Meilisearch管理器到handlers中
|
||||
handlers.SetMeilisearchManager(meilisearchManager)
|
||||
|
||||
@@ -184,6 +197,7 @@ func main() {
|
||||
autoFetchHotDrama, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoFetchHotDramaEnabled)
|
||||
autoProcessReadyResources, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoProcessReadyResources)
|
||||
autoTransferEnabled, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoTransferEnabled)
|
||||
autoSitemapEnabled, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeySitemapAutoGenerateEnabled)
|
||||
|
||||
globalScheduler.UpdateSchedulerStatusWithAutoTransfer(
|
||||
autoFetchHotDrama,
|
||||
@@ -191,6 +205,14 @@ func main() {
|
||||
autoTransferEnabled,
|
||||
)
|
||||
|
||||
// 根据系统配置启动Sitemap调度器
|
||||
if autoSitemapEnabled {
|
||||
globalScheduler.StartSitemapScheduler()
|
||||
utils.Info("系统配置启用Sitemap自动生成功能,启动定时任务")
|
||||
} else {
|
||||
utils.Info("系统配置禁用Sitemap自动生成功能")
|
||||
}
|
||||
|
||||
utils.Info("调度器初始化完成")
|
||||
|
||||
// 设置公开API中间件的Repository管理器
|
||||
@@ -465,6 +487,40 @@ func main() {
|
||||
api.PUT("/copyright-claims/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), copyrightClaimHandler.UpdateCopyrightClaim)
|
||||
api.DELETE("/copyright-claims/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), copyrightClaimHandler.DeleteCopyrightClaim)
|
||||
api.GET("/copyright-claims/resource/:resource_key", copyrightClaimHandler.GetCopyrightClaimByResource)
|
||||
|
||||
// Sitemap静态文件服务(优先于API路由)
|
||||
// 提供生成的sitemap.xml索引文件
|
||||
r.StaticFile("/sitemap.xml", "./data/sitemap/sitemap.xml")
|
||||
// 提供生成的sitemap分页文件,使用通配符路由
|
||||
r.GET("/sitemap-:page", func(c *gin.Context) {
|
||||
page := c.Param("page")
|
||||
if !strings.HasSuffix(page, ".xml") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
|
||||
return
|
||||
}
|
||||
c.File("./data/sitemap/sitemap-" + page)
|
||||
})
|
||||
|
||||
// Sitemap静态文件API路由(API兼容)
|
||||
api.GET("/sitemap.xml", func(c *gin.Context) {
|
||||
c.File("./data/sitemap/sitemap.xml")
|
||||
})
|
||||
// 提供生成的sitemap分页文件,使用API路径
|
||||
api.GET("/sitemap-:page", func(c *gin.Context) {
|
||||
page := c.Param("page")
|
||||
if !strings.HasSuffix(page, ".xml") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
|
||||
return
|
||||
}
|
||||
c.File("./data/sitemap/sitemap-" + page)
|
||||
})
|
||||
|
||||
// Sitemap管理API(通过管理员接口进行管理)
|
||||
api.GET("/sitemap/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSitemapConfig)
|
||||
api.POST("/sitemap/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateSitemapConfig)
|
||||
api.POST("/sitemap/generate", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GenerateSitemap)
|
||||
api.GET("/sitemap/status", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSitemapStatus)
|
||||
api.POST("/sitemap/full-generate", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GenerateFullSitemap)
|
||||
}
|
||||
|
||||
// 设置监控系统
|
||||
|
||||
@@ -101,6 +101,23 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /sitemap.xml {
|
||||
proxy_pass http://backend/api/sitemap.xml;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 其他 sitemap 分页文件
|
||||
location ~ ^/sitemap-\d+\.xml$ {
|
||||
proxy_pass http://backend/api$request_uri;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 默认路由 - 所有其他请求转发到前端
|
||||
location / {
|
||||
proxy_pass http://frontend;
|
||||
|
||||
@@ -148,3 +148,56 @@ func (gs *GlobalScheduler) UpdateSchedulerStatusWithAutoTransfer(autoFetchHotDra
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// StartSitemapScheduler 启动Sitemap调度任务
|
||||
func (gs *GlobalScheduler) StartSitemapScheduler() {
|
||||
gs.mutex.Lock()
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if gs.manager.IsSitemapRunning() {
|
||||
utils.Debug("Sitemap定时任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
gs.manager.StartSitemapScheduler()
|
||||
utils.Debug("全局调度器已启动Sitemap定时任务")
|
||||
}
|
||||
|
||||
// StopSitemapScheduler 停止Sitemap调度任务
|
||||
func (gs *GlobalScheduler) StopSitemapScheduler() {
|
||||
gs.mutex.Lock()
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if !gs.manager.IsSitemapRunning() {
|
||||
utils.Debug("Sitemap定时任务未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
gs.manager.StopSitemapScheduler()
|
||||
utils.Debug("全局调度器已停止Sitemap定时任务")
|
||||
}
|
||||
|
||||
// IsSitemapSchedulerRunning 检查Sitemap定时任务是否在运行
|
||||
func (gs *GlobalScheduler) IsSitemapSchedulerRunning() bool {
|
||||
gs.mutex.RLock()
|
||||
defer gs.mutex.RUnlock()
|
||||
return gs.manager.IsSitemapRunning()
|
||||
}
|
||||
|
||||
// UpdateSitemapConfig 更新Sitemap配置
|
||||
func (gs *GlobalScheduler) UpdateSitemapConfig(enabled bool) error {
|
||||
return gs.manager.UpdateSitemapConfig(enabled)
|
||||
}
|
||||
|
||||
// GetSitemapConfig 获取Sitemap配置
|
||||
func (gs *GlobalScheduler) GetSitemapConfig() (bool, error) {
|
||||
return gs.manager.GetSitemapConfig()
|
||||
}
|
||||
|
||||
// TriggerSitemapGeneration 手动触发sitemap生成
|
||||
func (gs *GlobalScheduler) TriggerSitemapGeneration() {
|
||||
gs.mutex.Lock()
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
gs.manager.TriggerSitemapGeneration()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ type Manager struct {
|
||||
baseScheduler *BaseScheduler
|
||||
hotDramaScheduler *HotDramaScheduler
|
||||
readyResourceScheduler *ReadyResourceScheduler
|
||||
sitemapScheduler *SitemapScheduler
|
||||
}
|
||||
|
||||
// NewManager 创建调度器管理器
|
||||
@@ -38,11 +39,13 @@ func NewManager(
|
||||
// 创建各个具体的调度器
|
||||
hotDramaScheduler := NewHotDramaScheduler(baseScheduler)
|
||||
readyResourceScheduler := NewReadyResourceScheduler(baseScheduler)
|
||||
sitemapScheduler := NewSitemapScheduler(baseScheduler)
|
||||
|
||||
return &Manager{
|
||||
baseScheduler: baseScheduler,
|
||||
hotDramaScheduler: hotDramaScheduler,
|
||||
readyResourceScheduler: readyResourceScheduler,
|
||||
sitemapScheduler: sitemapScheduler,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +110,41 @@ func (m *Manager) GetHotDramaNames() ([]string, error) {
|
||||
return m.hotDramaScheduler.GetHotDramaNames()
|
||||
}
|
||||
|
||||
// StartSitemapScheduler 启动Sitemap调度任务
|
||||
func (m *Manager) StartSitemapScheduler() {
|
||||
m.sitemapScheduler.Start()
|
||||
}
|
||||
|
||||
// StopSitemapScheduler 停止Sitemap调度任务
|
||||
func (m *Manager) StopSitemapScheduler() {
|
||||
m.sitemapScheduler.Stop()
|
||||
}
|
||||
|
||||
// IsSitemapRunning 检查Sitemap调度任务是否在运行
|
||||
func (m *Manager) IsSitemapRunning() bool {
|
||||
return m.sitemapScheduler.IsRunning()
|
||||
}
|
||||
|
||||
// GetSitemapConfig 获取Sitemap配置
|
||||
func (m *Manager) GetSitemapConfig() (bool, error) {
|
||||
return m.sitemapScheduler.GetSitemapConfig()
|
||||
}
|
||||
|
||||
// UpdateSitemapConfig 更新Sitemap配置
|
||||
func (m *Manager) UpdateSitemapConfig(enabled bool) error {
|
||||
return m.sitemapScheduler.UpdateSitemapConfig(enabled)
|
||||
}
|
||||
|
||||
// TriggerSitemapGeneration 手动触发sitemap生成
|
||||
func (m *Manager) TriggerSitemapGeneration() {
|
||||
go m.sitemapScheduler.generateSitemap()
|
||||
}
|
||||
|
||||
// GetStatus 获取所有调度任务的状态
|
||||
func (m *Manager) GetStatus() map[string]bool {
|
||||
return map[string]bool{
|
||||
"hot_drama": m.IsHotDramaRunning(),
|
||||
"ready_resource": m.IsReadyResourceRunning(),
|
||||
"sitemap": m.IsSitemapRunning(),
|
||||
}
|
||||
}
|
||||
|
||||
308
scheduler/sitemap.go
Normal file
308
scheduler/sitemap.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
SITEMAP_MAX_URLS = 50000 // 每个sitemap最多5万个URL
|
||||
SITEMAP_DIR = "./data/sitemap" // sitemap文件目录
|
||||
)
|
||||
|
||||
// SitemapScheduler Sitemap调度器
|
||||
type SitemapScheduler struct {
|
||||
*BaseScheduler
|
||||
sitemapConfig entity.SystemConfig
|
||||
stopChan chan bool
|
||||
isRunning bool
|
||||
}
|
||||
|
||||
// NewSitemapScheduler 创建Sitemap调度器
|
||||
func NewSitemapScheduler(baseScheduler *BaseScheduler) *SitemapScheduler {
|
||||
return &SitemapScheduler{
|
||||
BaseScheduler: baseScheduler,
|
||||
stopChan: make(chan bool),
|
||||
isRunning: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动Sitemap调度任务
|
||||
func (s *SitemapScheduler) Start() {
|
||||
if s.IsRunning() {
|
||||
utils.Debug("Sitemap定时任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
s.SetRunning(true)
|
||||
utils.Info("开始启动Sitemap定时任务")
|
||||
|
||||
go s.run()
|
||||
}
|
||||
|
||||
// Stop 停止Sitemap调度任务
|
||||
func (s *SitemapScheduler) Stop() {
|
||||
if !s.IsRunning() {
|
||||
utils.Debug("Sitemap定时任务未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("正在停止Sitemap定时任务")
|
||||
s.stopChan <- true
|
||||
s.SetRunning(false)
|
||||
}
|
||||
|
||||
// IsRunning 检查Sitemap调度任务是否在运行
|
||||
func (s *SitemapScheduler) IsRunning() bool {
|
||||
return s.isRunning
|
||||
}
|
||||
|
||||
// SetRunning 设置运行状态
|
||||
func (s *SitemapScheduler) SetRunning(running bool) {
|
||||
s.isRunning = running
|
||||
}
|
||||
|
||||
// GetStopChan 获取停止通道
|
||||
func (s *SitemapScheduler) GetStopChan() chan bool {
|
||||
return s.stopChan
|
||||
}
|
||||
|
||||
// run 执行调度任务的主循环
|
||||
func (s *SitemapScheduler) run() {
|
||||
utils.Info("Sitemap定时任务开始运行")
|
||||
|
||||
// 立即执行一次
|
||||
s.generateSitemap()
|
||||
|
||||
// 定时执行
|
||||
ticker := time.NewTicker(24 * time.Hour) // 每24小时执行一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
utils.Info("定时执行Sitemap生成任务")
|
||||
s.generateSitemap()
|
||||
case <-s.stopChan:
|
||||
utils.Info("收到停止信号,Sitemap调度任务退出")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateSitemap 生成sitemap
|
||||
func (s *SitemapScheduler) generateSitemap() {
|
||||
utils.Info("开始生成Sitemap...")
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// 获取资源总数
|
||||
var total int64
|
||||
if err := s.BaseScheduler.resourceRepo.GetDB().Model(&entity.Resource{}).Count(&total).Error; err != nil {
|
||||
utils.Error("获取资源总数失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("需要处理的资源总数: %d", total)
|
||||
|
||||
if total == 0 {
|
||||
utils.Info("没有资源需要生成Sitemap")
|
||||
return
|
||||
}
|
||||
|
||||
// 计算需要多少个sitemap文件
|
||||
totalPages := int((total + SITEMAP_MAX_URLS - 1) / SITEMAP_MAX_URLS)
|
||||
utils.Info("需要生成 %d 个sitemap文件", totalPages)
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(SITEMAP_DIR, 0755); err != nil {
|
||||
utils.Error("创建sitemap目录失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成每个sitemap文件
|
||||
for page := 0; page < totalPages; page++ {
|
||||
if s.SleepWithStopCheck(100 * time.Millisecond) { // 避免过于频繁的检查
|
||||
utils.Info("在生成sitemap过程中收到停止信号,退出生成")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("正在生成第 %d 个sitemap文件", page+1)
|
||||
|
||||
if err := s.generateSitemapPage(page); err != nil {
|
||||
utils.Error("生成第 %d 个sitemap文件失败: %v", page, err)
|
||||
} else {
|
||||
utils.Info("成功生成第 %d 个sitemap文件", page+1)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成sitemap索引文件
|
||||
if err := s.generateSitemapIndex(totalPages); err != nil {
|
||||
utils.Error("生成sitemap索引文件失败: %v", err)
|
||||
} else {
|
||||
utils.Info("成功生成sitemap索引文件")
|
||||
}
|
||||
|
||||
// 尝试获取网站基础URL
|
||||
baseURL, err := s.BaseScheduler.systemConfigRepo.GetConfigValue(entity.ConfigKeyWebsiteURL)
|
||||
if err != nil || baseURL == "" {
|
||||
baseURL = "https://yoursite.com" // 默认值
|
||||
}
|
||||
|
||||
utils.Info("Sitemap生成完成,耗时: %v", time.Since(startTime))
|
||||
utils.Info("Sitemap地址: %s/sitemap.xml", baseURL)
|
||||
}
|
||||
|
||||
// generateSitemapPage 生成单个sitemap页面
|
||||
func (s *SitemapScheduler) generateSitemapPage(page int) error {
|
||||
offset := page * SITEMAP_MAX_URLS
|
||||
limit := SITEMAP_MAX_URLS
|
||||
|
||||
var resources []entity.Resource
|
||||
if err := s.BaseScheduler.resourceRepo.GetDB().Offset(offset).Limit(limit).Find(&resources).Error; err != nil {
|
||||
return fmt.Errorf("获取资源数据失败: %w", err)
|
||||
}
|
||||
|
||||
var urls []Url
|
||||
for _, resource := range resources {
|
||||
lastMod := resource.UpdatedAt
|
||||
if resource.CreatedAt.After(lastMod) {
|
||||
lastMod = resource.CreatedAt
|
||||
}
|
||||
|
||||
urls = append(urls, Url{
|
||||
Loc: fmt.Sprintf("/r/%s", resource.Key),
|
||||
LastMod: lastMod.Format("2006-01-02"), // 只保留日期部分
|
||||
ChangeFreq: "weekly",
|
||||
Priority: 0.8,
|
||||
})
|
||||
}
|
||||
|
||||
urlSet := UrlSet{
|
||||
XMLNS: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
||||
URLs: urls,
|
||||
}
|
||||
|
||||
filename := filepath.Join(SITEMAP_DIR, fmt.Sprintf("sitemap-%d.xml", page))
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
file.WriteString(xml.Header)
|
||||
encoder := xml.NewEncoder(file)
|
||||
encoder.Indent("", " ")
|
||||
if err := encoder.Encode(urlSet); err != nil {
|
||||
return fmt.Errorf("写入XML失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSitemapIndex 生成sitemap索引文件
|
||||
func (s *SitemapScheduler) generateSitemapIndex(totalPages int) error {
|
||||
// 构建主机URL - 这里使用默认URL,实际应用中应从配置获取
|
||||
baseURL, err := s.BaseScheduler.systemConfigRepo.GetConfigValue(entity.ConfigKeyWebsiteURL)
|
||||
if err != nil || baseURL == "" {
|
||||
baseURL = "https://yoursite.com" // 默认值
|
||||
}
|
||||
|
||||
// 移除URL末尾的斜杠
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
|
||||
var sitemaps []Sitemap
|
||||
for i := 0; i < totalPages; i++ {
|
||||
sitemapURL := fmt.Sprintf("%s/sitemap-%d.xml", baseURL, i)
|
||||
sitemaps = append(sitemaps, Sitemap{
|
||||
Loc: sitemapURL,
|
||||
LastMod: time.Now().Format("2006-01-02"),
|
||||
})
|
||||
}
|
||||
|
||||
sitemapIndex := SitemapIndex{
|
||||
XMLNS: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
||||
Sitemaps: sitemaps,
|
||||
}
|
||||
|
||||
filename := filepath.Join(SITEMAP_DIR, "sitemap.xml")
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建索引文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
file.WriteString(xml.Header)
|
||||
encoder := xml.NewEncoder(file)
|
||||
encoder.Indent("", " ")
|
||||
if err := encoder.Encode(sitemapIndex); err != nil {
|
||||
return fmt.Errorf("写入索引XML失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSitemapConfig 获取Sitemap配置
|
||||
func (s *SitemapScheduler) GetSitemapConfig() (bool, error) {
|
||||
configStr, err := s.BaseScheduler.systemConfigRepo.GetConfigValue(entity.ConfigKeySitemapConfig)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 解析配置字符串,这里简化处理
|
||||
return configStr == "1" || configStr == "true", nil
|
||||
}
|
||||
|
||||
// UpdateSitemapConfig 更新Sitemap配置
|
||||
func (s *SitemapScheduler) UpdateSitemapConfig(enabled bool) error {
|
||||
configStr := "0"
|
||||
if enabled {
|
||||
configStr = "1"
|
||||
}
|
||||
|
||||
config := entity.SystemConfig{
|
||||
Key: entity.ConfigKeySitemapConfig,
|
||||
Value: configStr,
|
||||
Type: "bool",
|
||||
}
|
||||
|
||||
// 由于repository没有直接的SetConfig方法,我们使用UpsertConfigs
|
||||
configs := []entity.SystemConfig{config}
|
||||
return s.BaseScheduler.systemConfigRepo.UpsertConfigs(configs)
|
||||
}
|
||||
|
||||
// UrlSet sitemap内容
|
||||
type UrlSet struct {
|
||||
XMLName xml.Name `xml:"urlset"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
URLs []Url `xml:"url"`
|
||||
}
|
||||
|
||||
// Url 单个URL信息
|
||||
type Url struct {
|
||||
Loc string `xml:"loc"`
|
||||
LastMod string `xml:"lastmod"`
|
||||
ChangeFreq string `xml:"changefreq"`
|
||||
Priority float64 `xml:"priority"`
|
||||
}
|
||||
|
||||
// SitemapIndex sitemap索引结构
|
||||
type SitemapIndex struct {
|
||||
XMLName xml.Name `xml:"sitemapindex"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
Sitemaps []Sitemap `xml:"sitemap"`
|
||||
}
|
||||
|
||||
// Sitemap 单个sitemap信息
|
||||
type Sitemap struct {
|
||||
Loc string `xml:"loc"`
|
||||
LastMod string `xml:"lastmod"`
|
||||
}
|
||||
@@ -404,6 +404,27 @@ export const useWechatApi = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Sitemap管理API
|
||||
export const useSitemapApi = () => {
|
||||
const getSitemapConfig = () => useApiFetch('/sitemap/config').then(parseApiResponse)
|
||||
const updateSitemapConfig = (data: any) => useApiFetch('/sitemap/config', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const generateSitemap = () => useApiFetch('/sitemap/generate', { method: 'POST' }).then(parseApiResponse)
|
||||
const getSitemapStatus = () => useApiFetch('/sitemap/status').then(parseApiResponse)
|
||||
const fullGenerateSitemap = () => useApiFetch('/sitemap/full-generate', { method: 'POST' }).then(parseApiResponse)
|
||||
const getSitemapIndex = () => useApiFetch('/sitemap.xml')
|
||||
const getSitemapPage = (page: number) => useApiFetch(`/sitemap-${page}.xml`)
|
||||
|
||||
return {
|
||||
getSitemapConfig,
|
||||
updateSitemapConfig,
|
||||
generateSitemap,
|
||||
getSitemapStatus,
|
||||
fullGenerateSitemap,
|
||||
getSitemapIndex,
|
||||
getSitemapPage
|
||||
}
|
||||
}
|
||||
|
||||
// 统一API访问函数
|
||||
export const useApi = () => {
|
||||
return {
|
||||
@@ -425,6 +446,7 @@ export const useApi = () => {
|
||||
meilisearchApi: useMeilisearchApi(),
|
||||
apiAccessLogApi: useApiAccessLogApi(),
|
||||
systemLogApi: useSystemLogApi(),
|
||||
wechatApi: useWechatApi()
|
||||
wechatApi: useWechatApi(),
|
||||
sitemapApi: useSitemapApi()
|
||||
}
|
||||
}
|
||||
@@ -274,6 +274,125 @@
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="sitemap" tab="Sitemap管理">
|
||||
<div class="tab-content-container">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Sitemap管理</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">管理和生成网站sitemap文件,提升搜索引擎收录效果</p>
|
||||
</div>
|
||||
|
||||
<!-- Sitemap配置 -->
|
||||
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white mb-2">自动生成功能</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
开启后系统将定期自动生成sitemap文件
|
||||
</p>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="sitemapConfig.autoGenerate"
|
||||
@update:value="updateSitemapConfig"
|
||||
:loading="configLoading"
|
||||
size="large"
|
||||
>
|
||||
<template #checked>已开启</template>
|
||||
<template #unchecked>已关闭</template>
|
||||
</n-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sitemap统计 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<i class="fas fa-database text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">资源总数</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ sitemapStats.total_resources }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
|
||||
<i class="fas fa-file-code text-green-600 dark:text-green-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Sitemap数量</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ sitemapStats.total_pages }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
|
||||
<i class="fas fa-history text-purple-600 dark:text-purple-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">最后生成</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ sitemapStats.last_generate || '从未' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="generateSitemap"
|
||||
:loading="isGenerating"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-cogs"></i>
|
||||
</template>
|
||||
手动生成Sitemap
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="default"
|
||||
@click="viewSitemap"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</template>
|
||||
查看Sitemap
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="info"
|
||||
@click="refreshSitemapStatus"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</template>
|
||||
刷新状态
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 生成状态 -->
|
||||
<div v-if="generateStatus" class="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-info-circle text-yellow-600 dark:text-yellow-400 mt-0.5 mr-2"></i>
|
||||
<div>
|
||||
<h4 class="font-medium text-yellow-800 dark:text-yellow-200 mb-1">生成状态</h4>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-300">{{ generateStatus }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -503,9 +622,104 @@ const deleteLink = (row: any) => {
|
||||
message.warning(`删除外链: ${row.title}`)
|
||||
}
|
||||
|
||||
// Sitemap管理相关
|
||||
const sitemapConfig = ref({
|
||||
autoGenerate: false,
|
||||
lastGenerate: '',
|
||||
lastUpdate: ''
|
||||
})
|
||||
|
||||
const sitemapStats = ref({
|
||||
total_resources: 0,
|
||||
total_pages: 0,
|
||||
last_generate: ''
|
||||
})
|
||||
|
||||
const configLoading = ref(false)
|
||||
const isGenerating = ref(false)
|
||||
const generateStatus = ref('')
|
||||
|
||||
// 获取Sitemap配置
|
||||
const loadSitemapConfig = async () => {
|
||||
try {
|
||||
const sitemapApi = useSitemapApi()
|
||||
const response = await sitemapApi.getSitemapConfig()
|
||||
if (response) {
|
||||
sitemapConfig.value = response
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取Sitemap配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Sitemap配置
|
||||
const updateSitemapConfig = async (value: boolean) => {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const sitemapApi = useSitemapApi()
|
||||
await sitemapApi.updateSitemapConfig({
|
||||
autoGenerate: value,
|
||||
lastGenerate: sitemapConfig.value.lastGenerate,
|
||||
lastUpdate: new Date().toISOString()
|
||||
})
|
||||
message.success(value ? '自动生成功能已开启' : '自动生成功能已关闭')
|
||||
} catch (error) {
|
||||
message.error('更新配置失败')
|
||||
// 恢复之前的值
|
||||
sitemapConfig.value.autoGenerate = !value
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成Sitemap
|
||||
const generateSitemap = async () => {
|
||||
isGenerating.value = true
|
||||
generateStatus.value = '正在启动生成任务...'
|
||||
|
||||
try {
|
||||
const sitemapApi = useSitemapApi()
|
||||
const response = await sitemapApi.generateSitemap()
|
||||
|
||||
if (response) {
|
||||
generateStatus.value = response.message || '生成任务已启动'
|
||||
message.success('Sitemap生成任务已启动')
|
||||
// 更新统计信息
|
||||
sitemapStats.value.total_resources = response.total_resources || 0
|
||||
sitemapStats.value.total_pages = response.total_pages || 0
|
||||
}
|
||||
} catch (error: any) {
|
||||
generateStatus.value = '生成失败: ' + (error.message || '未知错误')
|
||||
message.error('Sitemap生成失败')
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新Sitemap状态
|
||||
const refreshSitemapStatus = async () => {
|
||||
try {
|
||||
const sitemapApi = useSitemapApi()
|
||||
const response = await sitemapApi.getSitemapStatus()
|
||||
if (response) {
|
||||
sitemapStats.value = response
|
||||
generateStatus.value = '状态已刷新'
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('刷新状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 查看Sitemap
|
||||
const viewSitemap = () => {
|
||||
window.open('/sitemap.xml', '_blank')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
loadLinkList()
|
||||
await loadSitemapConfig()
|
||||
await refreshSitemapStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
// 检查缓存是否过期
|
||||
const isValid = (now - timestamp) < CACHE_DURATION
|
||||
|
||||
console.log(`[SystemConfig] 缓存检查: ${isValid ? '有效' : '已过期'}, 剩余时间: ${Math.max(0, CACHE_DURATION - (now - timestamp)) / 1000 / 60}分钟`)
|
||||
// console.log(`[SystemConfig] 缓存检查: ${isValid ? '有效' : '已过期'}, 剩余时间: ${Math.max(0, CACHE_DURATION - (now - timestamp)) / 1000 / 60}分钟`)
|
||||
|
||||
return isValid
|
||||
} catch (error) {
|
||||
@@ -61,7 +61,7 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
const cacheData = localStorage.getItem(CACHE_KEY)
|
||||
if (cacheData) {
|
||||
const parsed = JSON.parse(cacheData) as CacheData
|
||||
console.log('[SystemConfig] 使用缓存数据')
|
||||
// console.log('[SystemConfig] 使用缓存数据')
|
||||
return parsed.config
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -99,7 +99,7 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
try {
|
||||
localStorage.removeItem(CACHE_KEY)
|
||||
localStorage.removeItem(CACHE_TIMESTAMP_KEY)
|
||||
console.log('[SystemConfig] 缓存已清除')
|
||||
// console.log('[SystemConfig] 缓存已清除')
|
||||
} catch (error) {
|
||||
console.error('[SystemConfig] 清除缓存失败:', error)
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData))
|
||||
localStorage.setItem(CACHE_TIMESTAMP_KEY, cacheData.timestamp.toString())
|
||||
|
||||
console.log('[SystemConfig] 配置已缓存,有效期30分钟')
|
||||
// console.log('[SystemConfig] 配置已缓存,有效期30分钟')
|
||||
} catch (error) {
|
||||
console.error('[SystemConfig] 保存缓存失败:', error)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
this.lastFetchTime = Date.now()
|
||||
}
|
||||
|
||||
console.log('[SystemConfig] 从缓存加载配置成功')
|
||||
// console.log('[SystemConfig] 从缓存加载配置成功')
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
async initConfig(force = false, useAdminApi = false) {
|
||||
// 如果已经初始化且不强制刷新,直接返回
|
||||
if (this.initialized && !force) {
|
||||
console.log('[SystemConfig] 配置已初始化,直接返回')
|
||||
// console.log('[SystemConfig] 配置已初始化,直接返回')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
|
||||
// 防止重复请求
|
||||
if (this.isLoading) {
|
||||
console.log('[SystemConfig] 正在加载中,等待完成...')
|
||||
// console.log('[SystemConfig] 正在加载中,等待完成...')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user