Files
urldb/handlers/google_index_handler.go
2025-11-25 02:05:59 +08:00

1031 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/ctwj/urldb/db/converter"
"github.com/ctwj/urldb/db/dto"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/pkg/google" // 添加google包导入
"github.com/ctwj/urldb/task"
"github.com/ctwj/urldb/utils"
"github.com/gin-gonic/gin"
goauth "golang.org/x/oauth2/google"
)
// GoogleIndexHandler Google索引处理程序
type GoogleIndexHandler struct {
repoMgr *repo.RepositoryManager
taskManager *task.TaskManager
}
// NewGoogleIndexHandler 创建Google索引处理程序
func NewGoogleIndexHandler(
repoMgr *repo.RepositoryManager,
taskManager *task.TaskManager,
) *GoogleIndexHandler {
return &GoogleIndexHandler{
repoMgr: repoMgr,
taskManager: taskManager,
}
}
// GetAllConfig 获取所有Google索引配置以分组形式返回
func (h *GoogleIndexHandler) GetAllConfig(c *gin.Context) {
// 获取通用配置
enabledStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyEnabled)
if err != nil {
enabledStr = "false"
}
enabled := enabledStr == "true" || enabledStr == "1"
siteURL, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.ConfigKeyWebsiteURL)
if err != nil {
siteURL = ""
}
siteName, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeySiteName)
if err != nil {
siteName = ""
}
// 获取调度配置
checkIntervalStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyCheckInterval)
if err != nil {
checkIntervalStr = "60"
}
checkInterval, _ := strconv.Atoi(checkIntervalStr)
batchSizeStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyBatchSize)
if err != nil {
batchSizeStr = "100"
}
batchSize, _ := strconv.Atoi(batchSizeStr)
concurrencyStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyConcurrency)
if err != nil {
concurrencyStr = "5"
}
concurrency, _ := strconv.Atoi(concurrencyStr)
retryAttemptsStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyRetryAttempts)
if err != nil {
retryAttemptsStr = "3"
}
retryAttempts, _ := strconv.Atoi(retryAttemptsStr)
retryDelayStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyRetryDelay)
if err != nil {
retryDelayStr = "2"
}
retryDelay, _ := strconv.Atoi(retryDelayStr)
// 获取网站地图配置
autoSitemapStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyAutoSitemap)
if err != nil {
autoSitemapStr = "false"
}
autoSitemap := autoSitemapStr == "true" || autoSitemapStr == "1"
sitemapPath, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeySitemapPath)
if err != nil {
sitemapPath = "/sitemap.xml"
}
sitemapSchedule, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeySitemapSchedule)
if err != nil {
sitemapSchedule = "@daily"
}
// 获取认证配置
credentialsFile, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyCredentialsFile)
if err != nil {
credentialsFile = ""
}
clientEmail, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyClientEmail)
if err != nil {
clientEmail = ""
}
clientID, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyClientID)
if err != nil {
clientID = ""
}
privateKey, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyPrivateKey)
if err != nil {
privateKey = ""
}
token, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyToken)
if err != nil {
token = ""
}
// 构建各组配置
generalConfig := dto.GoogleIndexConfigGeneral{
Enabled: enabled,
SiteURL: siteURL,
SiteName: siteName,
}
authConfig := dto.GoogleIndexConfigAuth{
CredentialsFile: credentialsFile,
ClientEmail: clientEmail,
ClientID: clientID,
PrivateKey: privateKey,
Token: token,
}
scheduleConfig := dto.GoogleIndexConfigSchedule{
CheckInterval: checkInterval,
BatchSize: batchSize,
Concurrency: concurrency,
RetryAttempts: retryAttempts,
RetryDelay: retryDelay,
}
sitemapConfig := dto.GoogleIndexConfigSitemap{
AutoSitemap: autoSitemap,
SitemapPath: sitemapPath,
SitemapSchedule: sitemapSchedule,
}
// 将配置对象转换为JSON字符串
generalConfigJSON, _ := json.Marshal(generalConfig)
authConfigJSON, _ := json.Marshal(authConfig)
scheduleConfigJSON, _ := json.Marshal(scheduleConfig)
sitemapConfigJSON, _ := json.Marshal(sitemapConfig)
// 以数组格式返回所有配置组
configs := []gin.H{
{
"group": "general",
"key": "general",
"value": string(generalConfigJSON),
},
{
"group": "auth",
"key": "credentials_file", // 使用具体的键名
"value": string(authConfigJSON),
},
{
"group": "schedule",
"key": "schedule",
"value": string(scheduleConfigJSON),
},
{
"group": "sitemap",
"key": "sitemap",
"value": string(sitemapConfigJSON),
},
}
SuccessResponse(c, configs)
}
// GetConfig 获取Google索引配置原有接口为保持兼容性
func (h *GoogleIndexHandler) GetConfig(c *gin.Context) {
// 获取通用配置
enabledStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyEnabled)
if err != nil {
enabledStr = "false"
}
enabled := enabledStr == "true" || enabledStr == "1"
siteURL, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.ConfigKeyWebsiteURL)
if err != nil {
siteURL = ""
}
siteName, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeySiteName)
if err != nil {
siteName = ""
}
// 获取调度配置
checkIntervalStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyCheckInterval)
if err != nil {
checkIntervalStr = "60"
}
checkInterval, _ := strconv.Atoi(checkIntervalStr)
batchSizeStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyBatchSize)
if err != nil {
batchSizeStr = "100"
}
batchSize, _ := strconv.Atoi(batchSizeStr)
concurrencyStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyConcurrency)
if err != nil {
concurrencyStr = "5"
}
concurrency, _ := strconv.Atoi(concurrencyStr)
retryAttemptsStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyRetryAttempts)
if err != nil {
retryAttemptsStr = "3"
}
retryAttempts, _ := strconv.Atoi(retryAttemptsStr)
retryDelayStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyRetryDelay)
if err != nil {
retryDelayStr = "2"
}
retryDelay, _ := strconv.Atoi(retryDelayStr)
// 获取网站地图配置
autoSitemapStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyAutoSitemap)
if err != nil {
autoSitemapStr = "false"
}
autoSitemap := autoSitemapStr == "true" || autoSitemapStr == "1"
sitemapPath, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeySitemapPath)
if err != nil {
sitemapPath = "/sitemap.xml"
}
sitemapSchedule, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeySitemapSchedule)
if err != nil {
sitemapSchedule = "@daily"
}
config := dto.GoogleIndexConfigGeneral{
Enabled: enabled,
SiteURL: siteURL,
SiteName: siteName,
}
scheduleConfig := dto.GoogleIndexConfigSchedule{
CheckInterval: checkInterval,
BatchSize: batchSize,
Concurrency: concurrency,
RetryAttempts: retryAttempts,
RetryDelay: retryDelay,
}
sitemapConfig := dto.GoogleIndexConfigSitemap{
AutoSitemap: autoSitemap,
SitemapPath: sitemapPath,
SitemapSchedule: sitemapSchedule,
}
result := gin.H{
"general": config,
"schedule": scheduleConfig,
"sitemap": sitemapConfig,
"is_running": false, // 不再有独立的调度器,使用统一任务管理器
}
SuccessResponse(c, result)
}
// UpdateConfig 更新Google索引配置
func (h *GoogleIndexHandler) UpdateConfig(c *gin.Context) {
var req struct {
General dto.GoogleIndexConfigGeneral `json:"general"`
Schedule dto.GoogleIndexConfigSchedule `json:"schedule"`
Sitemap dto.GoogleIndexConfigSitemap `json:"sitemap"`
}
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
return
}
username, _ := c.Get("username")
clientIP, _ := c.Get("client_ip")
utils.Info("GoogleIndexHandler.UpdateConfig - 用户更新Google索引配置 - 用户: %s, IP: %s", username, clientIP)
// 准备要更新的配置项
configs := []entity.SystemConfig{
{
Key: entity.GoogleIndexConfigKeyEnabled,
Value: strconv.FormatBool(req.General.Enabled),
Type: entity.ConfigTypeBool,
},
{
Key: entity.GoogleIndexConfigKeySiteName,
Value: req.General.SiteName,
Type: entity.ConfigTypeString,
},
{
Key: entity.GoogleIndexConfigKeyCheckInterval,
Value: strconv.Itoa(req.Schedule.CheckInterval),
Type: entity.ConfigTypeInt,
},
{
Key: entity.GoogleIndexConfigKeyBatchSize,
Value: strconv.Itoa(req.Schedule.BatchSize),
Type: entity.ConfigTypeInt,
},
{
Key: entity.GoogleIndexConfigKeyConcurrency,
Value: strconv.Itoa(req.Schedule.Concurrency),
Type: entity.ConfigTypeInt,
},
{
Key: entity.GoogleIndexConfigKeyRetryAttempts,
Value: strconv.Itoa(req.Schedule.RetryAttempts),
Type: entity.ConfigTypeInt,
},
{
Key: entity.GoogleIndexConfigKeyRetryDelay,
Value: strconv.Itoa(req.Schedule.RetryDelay),
Type: entity.ConfigTypeInt,
},
{
Key: entity.GoogleIndexConfigKeyAutoSitemap,
Value: strconv.FormatBool(req.Sitemap.AutoSitemap),
Type: entity.ConfigTypeBool,
},
{
Key: entity.GoogleIndexConfigKeySitemapPath,
Value: req.Sitemap.SitemapPath,
Type: entity.ConfigTypeString,
},
{
Key: entity.GoogleIndexConfigKeySitemapSchedule,
Value: req.Sitemap.SitemapSchedule,
Type: entity.ConfigTypeString,
},
}
// 批量更新配置
err := h.repoMgr.SystemConfigRepository.UpsertConfigs(configs)
if err != nil {
utils.Error("更新系统配置失败: %v", err)
ErrorResponse(c, "更新配置失败", http.StatusInternalServerError)
return
}
utils.Info("Google索引配置更新成功 - 用户: %s, IP: %s", username, clientIP)
SuccessResponse(c, gin.H{
"message": "配置更新成功",
})
}
// CreateTask 创建Google索引任务
func (h *GoogleIndexHandler) CreateTask(c *gin.Context) {
var req dto.GoogleIndexTaskInput
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
return
}
username, _ := c.Get("username")
clientIP, _ := c.Get("client_ip")
utils.Info("GoogleIndexHandler.CreateTask - 用户创建Google索引任务 - 用户: %s, 任务类型: %s, 任务标题: %s, IP: %s", username, req.Type, req.Title, clientIP)
// 创建通用任务
task, err := h.taskManager.CreateTask(string(entity.TaskTypeGoogleIndex), req.Title, req.Description, req.ConfigID)
if err != nil {
utils.Error("创建Google索引任务失败: %v", err)
ErrorResponse(c, "创建任务失败", http.StatusInternalServerError)
return
}
// 根据任务类型创建任务项
var taskItems []*entity.TaskItem
switch req.Type {
case "url_indexing", "status_check", "batch_index", "manual_check":
// 为每个URL创建任务项
for _, url := range req.URLs {
itemData := map[string]interface{}{
"urls": []string{url},
"operation": req.Type,
}
itemDataJSON, _ := json.Marshal(itemData)
taskItem := &entity.TaskItem{
URL: url,
InputData: string(itemDataJSON),
}
taskItems = append(taskItems, taskItem)
}
case "sitemap_submit":
// 为网站地图创建任务项
itemData := map[string]interface{}{
"sitemap_url": req.SitemapURL,
"operation": "sitemap_submit",
}
itemDataJSON, _ := json.Marshal(itemData)
taskItem := &entity.TaskItem{
URL: req.SitemapURL,
InputData: string(itemDataJSON),
}
taskItems = append(taskItems, taskItem)
}
// 批量创建任务项
err = h.taskManager.CreateTaskItems(task.ID, taskItems)
if err != nil {
utils.Error("创建任务项失败: %v", err)
ErrorResponse(c, "创建任务项失败", http.StatusInternalServerError)
return
}
// 更新任务的总项目数
err = h.repoMgr.TaskRepository.UpdateTotalItems(task.ID, len(taskItems))
if err != nil {
utils.Error("更新任务总项目数失败: %v", err)
// 不返回错误,因为任务和任务项已经创建成功
}
utils.Info("Google索引任务创建完成: %d, 任务类型: %s, 总项目数: %d", task.ID, req.Type, len(taskItems))
SuccessResponse(c, gin.H{
"task_id": task.ID,
"total_items": len(taskItems),
"message": "任务创建成功",
})
}
// StartTask 启动Google索引任务
func (h *GoogleIndexHandler) StartTask(c *gin.Context) {
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的任务ID", http.StatusBadRequest)
return
}
username, _ := c.Get("username")
clientIP, _ := c.Get("client_ip")
utils.Info("GoogleIndexHandler.StartTask - 用户启动Google索引任务 - 用户: %s, 任务ID: %d, IP: %s", username, taskID, clientIP)
// 使用任务管理器启动任务
err = h.taskManager.StartTask(uint(taskID))
if err != nil {
utils.Error("启动Google索引任务失败: %v", err)
ErrorResponse(c, "启动任务失败: "+err.Error(), http.StatusInternalServerError)
return
}
utils.Info("Google索引任务启动成功: %d", taskID)
SuccessResponse(c, gin.H{
"message": "任务启动成功",
})
}
// GetTaskStatus 获取任务状态
func (h *GoogleIndexHandler) GetTaskStatus(c *gin.Context) {
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的任务ID", http.StatusBadRequest)
return
}
task, err := h.taskManager.GetTask(uint(taskID))
if err != nil {
ErrorResponse(c, "获取任务失败", http.StatusInternalServerError)
return
}
if task == nil {
ErrorResponse(c, "任务不存在", http.StatusNotFound)
return
}
// 获取任务项统计
stats, err := h.taskManager.GetTaskItemStats(task.ID)
if err != nil {
utils.Error("获取任务项统计失败: %v", err)
stats = make(map[string]int)
}
taskOutput := converter.TaskToGoogleIndexTaskOutput(task, stats)
result := gin.H{
"id": taskOutput.ID,
"name": taskOutput.Name,
"type": taskOutput.Type,
"status": taskOutput.Status,
"description": taskOutput.Description,
"progress": taskOutput.Progress,
"total_items": taskOutput.TotalItems,
"processed_items": taskOutput.ProcessedItems,
"successful_items": taskOutput.SuccessfulItems,
"failed_items": taskOutput.FailedItems,
"pending_items": taskOutput.PendingItems,
"processing_items": taskOutput.ProcessingItems,
"indexed_urls": taskOutput.IndexedURLs,
"failed_urls": taskOutput.FailedURLs,
"started_at": taskOutput.StartedAt,
"completed_at": taskOutput.CompletedAt,
"created_at": taskOutput.CreatedAt,
"updated_at": taskOutput.UpdatedAt,
"progress_data": taskOutput.ProgressData,
"stats": stats,
}
SuccessResponse(c, result)
}
// GetTasks 获取任务列表
func (h *GoogleIndexHandler) GetTasks(c *gin.Context) {
pageStr := c.DefaultQuery("page", "1")
pageSizeStr := c.DefaultQuery("page_size", "10")
taskTypeStr := c.Query("type")
statusStr := c.Query("status")
page, _ := strconv.Atoi(pageStr)
if page < 1 {
page = 1
}
pageSize, _ := strconv.Atoi(pageSizeStr)
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
// 根据参数筛选任务类型如果有指定则使用否则默认为Google索引类型
taskType := string(entity.TaskTypeGoogleIndex)
if taskTypeStr != "" {
taskType = taskTypeStr
}
// 获取指定状态的任务,默认查找所有状态
status := statusStr
// 获取任务列表 - 目前我们没有Query方法直接获取所有任务然后做筛选
tasksList, total, err := h.repoMgr.TaskRepository.GetList(page, pageSize, taskType, status)
if err != nil {
ErrorResponse(c, "获取任务列表失败", http.StatusInternalServerError)
return
}
taskOutputs := make([]dto.GoogleIndexTaskOutput, len(tasksList))
for i, task := range tasksList {
// 获取任务统计信息
stats, err := h.taskManager.GetTaskItemStats(task.ID)
if err != nil {
stats = make(map[string]int)
}
taskOutputs[i] = converter.TaskToGoogleIndexTaskOutput(task, stats)
}
result := dto.GoogleIndexTaskListResponse{
Tasks: taskOutputs,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)),
}
SuccessResponse(c, result)
}
// GetTaskItems 获取任务项列表
func (h *GoogleIndexHandler) GetTaskItems(c *gin.Context) {
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的任务ID", http.StatusBadRequest)
return
}
pageStr := c.DefaultQuery("page", "1")
pageSizeStr := c.DefaultQuery("page_size", "50")
statusStr := c.Query("status")
page, _ := strconv.Atoi(pageStr)
if page < 1 {
page = 1
}
pageSize, _ := strconv.Atoi(pageSizeStr)
if pageSize < 1 || pageSize > 1000 {
pageSize = 50
}
// 获取任务项列表
items, total, err := h.taskManager.QueryTaskItems(uint(taskID), page, pageSize, statusStr)
if err != nil {
ErrorResponse(c, "获取任务项列表失败", http.StatusInternalServerError)
return
}
// 注意我们还没有TaskItemToGoogleIndexTaskItemOutput转换器需要创建一个
itemOutputs := make([]dto.GoogleIndexTaskItemOutput, len(items))
for i, item := range items {
// 手动构建输出结构
itemOutputs[i] = dto.GoogleIndexTaskItemOutput{
ID: item.ID,
TaskID: item.TaskID,
URL: item.URL,
Status: string(item.Status),
IndexStatus: item.IndexStatus,
ErrorMessage: item.ErrorMessage,
InspectResult: item.InspectResult,
MobileFriendly: item.MobileFriendly,
LastCrawled: item.LastCrawled,
StatusCode: item.StatusCode,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
StartedAt: item.ProcessedAt, // 任务项处理完成时间
CompletedAt: item.ProcessedAt,
}
}
result := dto.GoogleIndexTaskItemPageResponse{
Items: itemOutputs,
Total: total,
Page: page,
Size: pageSize,
}
SuccessResponse(c, result)
}
// UploadCredentials 上传Google索引凭据
func (h *GoogleIndexHandler) UploadCredentials(c *gin.Context) {
// 获取上传的文件
file, err := c.FormFile("file")
if err != nil {
ErrorResponse(c, "未提供凭据文件", http.StatusBadRequest)
return
}
// 验证文件扩展名必须是.json
ext := strings.ToLower(filepath.Ext(file.Filename))
if ext != ".json" {
ErrorResponse(c, "仅支持JSON格式的凭据文件", http.StatusBadRequest)
return
}
// 验证文件大小限制5MB
if file.Size > 5*1024*1024 {
ErrorResponse(c, "文件大小不能超过5MB", http.StatusBadRequest)
return
}
// 确保data目录存在
dataDir := "./data"
if err := os.MkdirAll(dataDir, 0755); err != nil {
ErrorResponse(c, "创建数据目录失败", http.StatusInternalServerError)
return
}
// 使用固定的文件名保存凭据
fixedFileName := "google_credentials.json"
filePath := filepath.Join(dataDir, fixedFileName)
// 保存文件
if err := c.SaveUploadedFile(file, filePath); err != nil {
ErrorResponse(c, "保存凭据文件失败", http.StatusInternalServerError)
return
}
// 设置文件权限
if err := os.Chmod(filePath, 0600); err != nil {
utils.Warn("设置凭据文件权限失败: %v", err)
}
// 返回成功响应
accessPath := filepath.Join("data", fixedFileName)
response := map[string]interface{}{
"success": true,
"message": "凭据文件上传成功",
"file_name": fixedFileName,
"file_path": accessPath,
"full_path": filePath,
}
SuccessResponse(c, response)
}
// makeSafeFileName 生成安全的文件名,移除危险字符
func (h *GoogleIndexHandler) makeSafeFileName(filename string) string {
// 移除路径分隔符和特殊字符
safeName := strings.ReplaceAll(filename, "/", "_")
safeName = strings.ReplaceAll(safeName, "\\", "_")
safeName = strings.ReplaceAll(safeName, "..", "_")
// 限制文件名长度
if len(safeName) > 100 {
ext := filepath.Ext(safeName)
name := safeName[:100-len(ext)]
safeName = name + ext
}
return safeName
}
// ValidateCredentials 验证Google索引凭据
func (h *GoogleIndexHandler) ValidateCredentials(c *gin.Context) {
// 使用固定的凭据文件路径
credentialsFile := "data/google_credentials.json"
// 检查凭据文件是否存在
if _, err := os.Stat(credentialsFile); os.IsNotExist(err) {
ErrorResponse(c, "凭据文件不存在", http.StatusBadRequest)
return
}
// 尝试创建Google客户端并验证凭据
config, err := h.loadCredentials(credentialsFile)
if err != nil {
ErrorResponse(c, "凭据格式错误: "+err.Error(), http.StatusBadRequest)
return
}
// 验证凭据是否有效尝试获取token
err = h.getValidToken(config)
if err != nil {
ErrorResponse(c, "凭据验证失败: "+err.Error(), http.StatusBadRequest)
return
}
response := map[string]interface{}{
"success": true,
"message": "凭据验证成功",
"valid": true,
}
SuccessResponse(c, response)
}
// GetStatus 获取Google索引状态
func (h *GoogleIndexHandler) GetStatus(c *gin.Context) {
// 获取通用配置
enabledStr, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyEnabled)
if err != nil {
enabledStr = "false"
}
enabled := enabledStr == "true" || enabledStr == "1"
siteURL, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.ConfigKeyWebsiteURL)
if err != nil {
siteURL = ""
}
// 获取认证配置
credentialsFile, err := h.repoMgr.SystemConfigRepository.GetConfigValue(entity.GoogleIndexConfigKeyCredentialsFile)
if err != nil {
credentialsFile = ""
}
// 验证凭据是否有效
authValid := false
if credentialsFile != "" {
if _, err := os.Stat(credentialsFile); !os.IsNotExist(err) {
// 检查凭据文件是否存在且可读
authValid = true
}
}
// 获取统计信息(从数据库查询实际的资源数量)
totalURLs := 0
indexedURLs := 0
notIndexedURLs := 0
errorURLs := 0
// 查询resources表获取总URL数
totalResources, err := h.repoMgr.ResourceRepository.GetTotalCount()
if err == nil {
totalURLs = int(totalResources)
}
// 查询任务项统计获取索引状态
taskStats, err := h.repoMgr.TaskItemRepository.GetIndexStats()
if err == nil {
indexedURLs = taskStats["indexed"]
notIndexedURLs = taskStats["not_indexed"]
errorURLs = taskStats["error"]
}
statusResponse := dto.GoogleIndexStatusResponse{
Enabled: enabled,
SiteURL: siteURL,
LastCheckTime: time.Now(),
TotalURLs: totalURLs,
IndexedURLs: indexedURLs,
NotIndexedURLs: notIndexedURLs,
ErrorURLs: errorURLs,
LastSitemapSubmit: time.Time{},
AuthValid: authValid,
}
SuccessResponse(c, statusResponse)
}
// loadCredentials 从文件加载凭据
func (h *GoogleIndexHandler) loadCredentials(credentialsFile string) (*google.Config, error) {
// 读取凭据文件
data, err := os.ReadFile(credentialsFile)
if err != nil {
return nil, fmt.Errorf("无法读取凭据文件: %v", err)
}
// 验证是否为有效的JSON
var temp map[string]interface{}
if err := json.Unmarshal(data, &temp); err != nil {
return nil, fmt.Errorf("凭据文件格式不是有效的JSON: %v", err)
}
// 检查必需字段
requiredFields := []string{"type", "project_id", "private_key_id", "private_key", "client_email", "client_id"}
for _, field := range requiredFields {
if _, exists := temp[field]; !exists {
return nil, fmt.Errorf("凭据文件缺少必需字段: %s", field)
}
}
// 检查凭据类型
if temp["type"] != "service_account" {
return nil, fmt.Errorf("仅支持服务账号类型的凭据")
}
// 尝试从JSON数据加载凭据需要指定作用域
scopes := []string{
"https://www.googleapis.com/auth/webmasters",
"https://www.googleapis.com/auth/indexing",
}
jwtConfig, err := goauth.JWTConfigFromJSON(data, scopes...)
if err != nil {
return nil, fmt.Errorf("创建JWT配置失败: %v", err)
}
// 创建一个简单的配置对象暂时只存储JWT配置
config := &google.Config{
CredentialsFile: credentialsFile,
}
// 为了验证凭据我们尝试获取token源
ctx := context.Background()
tokenSource := jwtConfig.TokenSource(ctx)
_ = tokenSource // 实际验证在getValidToken中进行
return config, nil
}
// getValidToken 获取有效的token
// UpdateGoogleIndexConfig 更新Google索引配置支持分组配置
func (h *GoogleIndexHandler) UpdateGoogleIndexConfig(c *gin.Context) {
var req dto.GoogleIndexConfigInput
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
return
}
username, _ := c.Get("username")
clientIP, _ := c.Get("client_ip")
utils.Info("GoogleIndexHandler.UpdateGoogleIndexConfig - 用户更新Google索引分组配置 - 用户: %s, 组: %s, 键: %s, IP: %s", username, req.Group, req.Key, clientIP)
// 处理不同的配置组
switch req.Group {
case "general":
switch req.Key {
case "general":
// 解析general配置
var generalConfig dto.GoogleIndexConfigGeneral
if err := json.Unmarshal([]byte(req.Value), &generalConfig); err != nil {
ErrorResponse(c, "通用配置格式错误: "+err.Error(), http.StatusBadRequest)
return
}
// 存储各个字段
generalConfigs := []entity.SystemConfig{
{Key: entity.GoogleIndexConfigKeyEnabled, Value: strconv.FormatBool(generalConfig.Enabled), Type: entity.ConfigTypeBool},
{Key: entity.GoogleIndexConfigKeySiteName, Value: generalConfig.SiteName, Type: entity.ConfigTypeString},
}
err := h.repoMgr.SystemConfigRepository.UpsertConfigs(generalConfigs)
if err != nil {
ErrorResponse(c, "保存通用配置失败: "+err.Error(), http.StatusInternalServerError)
return
}
default:
ErrorResponse(c, "未知的通用配置键: "+req.Key, http.StatusBadRequest)
return
}
case "auth":
switch req.Key {
case "credentials_file":
// 解析认证配置
var authConfig dto.GoogleIndexConfigAuth
if err := json.Unmarshal([]byte(req.Value), &authConfig); err != nil {
ErrorResponse(c, "认证配置格式错误: "+err.Error(), http.StatusBadRequest)
return
}
// 存储认证相关配置
authConfigs := []entity.SystemConfig{
{Key: entity.GoogleIndexConfigKeyCredentialsFile, Value: authConfig.CredentialsFile, Type: entity.ConfigTypeString},
{Key: entity.GoogleIndexConfigKeyClientEmail, Value: authConfig.ClientEmail, Type: entity.ConfigTypeString},
{Key: entity.GoogleIndexConfigKeyClientID, Value: authConfig.ClientID, Type: entity.ConfigTypeString},
{Key: entity.GoogleIndexConfigKeyPrivateKey, Value: authConfig.PrivateKey, Type: entity.ConfigTypeString},
}
err := h.repoMgr.SystemConfigRepository.UpsertConfigs(authConfigs)
if err != nil {
ErrorResponse(c, "保存认证配置失败: "+err.Error(), http.StatusInternalServerError)
return
}
default:
ErrorResponse(c, "未知的认证配置键: "+req.Key, http.StatusBadRequest)
return
}
case "schedule":
switch req.Key {
case "schedule":
// 解析调度配置
var scheduleConfig dto.GoogleIndexConfigSchedule
if err := json.Unmarshal([]byte(req.Value), &scheduleConfig); err != nil {
ErrorResponse(c, "调度配置格式错误: "+err.Error(), http.StatusBadRequest)
return
}
// 存储调度相关配置
scheduleConfigs := []entity.SystemConfig{
{Key: entity.GoogleIndexConfigKeyCheckInterval, Value: strconv.Itoa(scheduleConfig.CheckInterval), Type: entity.ConfigTypeInt},
{Key: entity.GoogleIndexConfigKeyBatchSize, Value: strconv.Itoa(scheduleConfig.BatchSize), Type: entity.ConfigTypeInt},
{Key: entity.GoogleIndexConfigKeyConcurrency, Value: strconv.Itoa(scheduleConfig.Concurrency), Type: entity.ConfigTypeInt},
{Key: entity.GoogleIndexConfigKeyRetryAttempts, Value: strconv.Itoa(scheduleConfig.RetryAttempts), Type: entity.ConfigTypeInt},
{Key: entity.GoogleIndexConfigKeyRetryDelay, Value: strconv.Itoa(scheduleConfig.RetryDelay), Type: entity.ConfigTypeInt},
}
err := h.repoMgr.SystemConfigRepository.UpsertConfigs(scheduleConfigs)
if err != nil {
ErrorResponse(c, "保存调度配置失败: "+err.Error(), http.StatusInternalServerError)
return
}
default:
ErrorResponse(c, "未知的调度配置键: "+req.Key, http.StatusBadRequest)
return
}
case "sitemap":
switch req.Key {
case "sitemap":
// 解析网站地图配置
var sitemapConfig dto.GoogleIndexConfigSitemap
if err := json.Unmarshal([]byte(req.Value), &sitemapConfig); err != nil {
ErrorResponse(c, "网站地图配置格式错误: "+err.Error(), http.StatusBadRequest)
return
}
// 存储网站地图相关配置
sitemapConfigs := []entity.SystemConfig{
{Key: entity.GoogleIndexConfigKeyAutoSitemap, Value: strconv.FormatBool(sitemapConfig.AutoSitemap), Type: entity.ConfigTypeBool},
{Key: entity.GoogleIndexConfigKeySitemapPath, Value: sitemapConfig.SitemapPath, Type: entity.ConfigTypeString},
{Key: entity.GoogleIndexConfigKeySitemapSchedule, Value: sitemapConfig.SitemapSchedule, Type: entity.ConfigTypeString},
}
err := h.repoMgr.SystemConfigRepository.UpsertConfigs(sitemapConfigs)
if err != nil {
ErrorResponse(c, "保存网站地图配置失败: "+err.Error(), http.StatusInternalServerError)
return
}
default:
ErrorResponse(c, "未知的网站地图配置键: "+req.Key, http.StatusBadRequest)
return
}
default:
ErrorResponse(c, "未知的配置组: "+req.Group, http.StatusBadRequest)
return
}
utils.Info("Google索引分组配置更新成功 - 组: %s, 键: %s - 用户: %s, IP: %s", req.Group, req.Key, username, clientIP)
SuccessResponse(c, gin.H{
"message": "配置更新成功",
"group": req.Group,
"key": req.Key,
})
}
func (h *GoogleIndexHandler) getValidToken(config *google.Config) error {
// 为了简单验证我们只尝试读取凭据文件并确保JWT配置可以正常工作
// 在实际实现中这里应该尝试获取一个实际的token
// 重新读取凭据文件进行验证
data, err := os.ReadFile(config.CredentialsFile)
if err != nil {
return fmt.Errorf("无法读取凭据文件: %v", err)
}
// 尝试从JSON数据加载凭据需要指定作用域
scopes := []string{
"https://www.googleapis.com/auth/webmasters",
"https://www.googleapis.com/auth/indexing",
}
_, err = goauth.JWTConfigFromJSON(data, scopes...)
if err != nil {
return fmt.Errorf("创建JWT配置失败: %v", err)
}
// 如果能成功创建JWT配置我们认为凭据格式是正确的
// 在实际环境中这里应该尝试获取token来验证凭据的有效性
return nil
}