diff --git a/db/converter/system_config_converter.go b/db/converter/system_config_converter.go index 63e350c..8994086 100644 --- a/db/converter/system_config_converter.go +++ b/db/converter/system_config_converter.go @@ -97,37 +97,100 @@ func RequestToSystemConfig(req *dto.SystemConfigRequest) []entity.SystemConfig { } var configs []entity.SystemConfig + var updatedKeys []string - // 字符串字段 - 处理所有字段,包括空值 - // 对于广告相关字段,允许空值以便清空配置 - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeySiteTitle, Value: req.SiteTitle, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeySiteDescription, Value: req.SiteDescription, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyKeywords, Value: req.Keywords, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAuthor, Value: req.Author, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyCopyright, Value: req.Copyright, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeySiteLogo, Value: req.SiteLogo, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyApiToken, Value: req.ApiToken, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyForbiddenWords, Value: req.ForbiddenWords, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAdKeywords, Value: req.AdKeywords, Type: entity.ConfigTypeString}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoInsertAd, Value: req.AutoInsertAd, Type: entity.ConfigTypeString}) + // 字符串字段 - 只处理被设置的字段 + if req.SiteTitle != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeySiteTitle, Value: *req.SiteTitle, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeySiteTitle) + } + if req.SiteDescription != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeySiteDescription, Value: *req.SiteDescription, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeySiteDescription) + } + if req.Keywords != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyKeywords, Value: *req.Keywords, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeyKeywords) + } + if req.Author != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAuthor, Value: *req.Author, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAuthor) + } + if req.Copyright != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyCopyright, Value: *req.Copyright, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeyCopyright) + } + if req.SiteLogo != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeySiteLogo, Value: *req.SiteLogo, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeySiteLogo) + } + if req.ApiToken != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyApiToken, Value: *req.ApiToken, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeyApiToken) + } + if req.ForbiddenWords != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyForbiddenWords, Value: *req.ForbiddenWords, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeyForbiddenWords) + } + if req.AdKeywords != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAdKeywords, Value: *req.AdKeywords, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAdKeywords) + } + if req.AutoInsertAd != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoInsertAd, Value: *req.AutoInsertAd, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAutoInsertAd) + } - // 布尔值字段 - 只处理实际提交的字段 - // 注意:由于 Go 的零值机制,我们需要通过其他方式判断字段是否被提交 - // 这里暂时保持原样,但建议前端只提交有变化的字段 - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoProcessReadyResources, Value: strconv.FormatBool(req.AutoProcessReadyResources), Type: entity.ConfigTypeBool}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferEnabled, Value: strconv.FormatBool(req.AutoTransferEnabled), Type: entity.ConfigTypeBool}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: strconv.FormatBool(req.AutoFetchHotDramaEnabled), Type: entity.ConfigTypeBool}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMaintenanceMode, Value: strconv.FormatBool(req.MaintenanceMode), Type: entity.ConfigTypeBool}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyEnableRegister, Value: strconv.FormatBool(req.EnableRegister), Type: entity.ConfigTypeBool}) + // 布尔值字段 - 只处理被设置的字段 + if req.AutoProcessReadyResources != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoProcessReadyResources, Value: strconv.FormatBool(*req.AutoProcessReadyResources), Type: entity.ConfigTypeBool}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAutoProcessReadyResources) + } + if req.AutoTransferEnabled != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferEnabled, Value: strconv.FormatBool(*req.AutoTransferEnabled), Type: entity.ConfigTypeBool}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAutoTransferEnabled) + } + if req.AutoFetchHotDramaEnabled != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: strconv.FormatBool(*req.AutoFetchHotDramaEnabled), Type: entity.ConfigTypeBool}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAutoFetchHotDramaEnabled) + } + if req.MaintenanceMode != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMaintenanceMode, Value: strconv.FormatBool(*req.MaintenanceMode), Type: entity.ConfigTypeBool}) + updatedKeys = append(updatedKeys, entity.ConfigKeyMaintenanceMode) + } + if req.EnableRegister != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyEnableRegister, Value: strconv.FormatBool(*req.EnableRegister), Type: entity.ConfigTypeBool}) + updatedKeys = append(updatedKeys, entity.ConfigKeyEnableRegister) + } - // 整数字段 - 添加所有提交的字段,包括0值 - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoProcessInterval, Value: strconv.Itoa(req.AutoProcessInterval), Type: entity.ConfigTypeInt}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferLimitDays, Value: strconv.Itoa(req.AutoTransferLimitDays), Type: entity.ConfigTypeInt}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferMinSpace, Value: strconv.Itoa(req.AutoTransferMinSpace), Type: entity.ConfigTypeInt}) - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyPageSize, Value: strconv.Itoa(req.PageSize), Type: entity.ConfigTypeInt}) + // 整数字段 - 只处理被设置的字段 + if req.AutoProcessInterval != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoProcessInterval, Value: strconv.Itoa(*req.AutoProcessInterval), Type: entity.ConfigTypeInt}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAutoProcessInterval) + } + if req.AutoTransferLimitDays != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferLimitDays, Value: strconv.Itoa(*req.AutoTransferLimitDays), Type: entity.ConfigTypeInt}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAutoTransferLimitDays) + } + if req.AutoTransferMinSpace != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferMinSpace, Value: strconv.Itoa(*req.AutoTransferMinSpace), Type: entity.ConfigTypeInt}) + updatedKeys = append(updatedKeys, entity.ConfigKeyAutoTransferMinSpace) + } + if req.PageSize != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyPageSize, Value: strconv.Itoa(*req.PageSize), Type: entity.ConfigTypeInt}) + updatedKeys = append(updatedKeys, entity.ConfigKeyPageSize) + } - // 三方统计配置 - configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyThirdPartyStatsCode, Value: req.ThirdPartyStatsCode, Type: entity.ConfigTypeString}) + // 三方统计配置 - 只处理被设置的字段 + if req.ThirdPartyStatsCode != nil { + configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyThirdPartyStatsCode, Value: *req.ThirdPartyStatsCode, Type: entity.ConfigTypeString}) + updatedKeys = append(updatedKeys, entity.ConfigKeyThirdPartyStatsCode) + } + + // 记录更新的配置项 + if len(updatedKeys) > 0 { + utils.Info("配置更新 - 被修改的配置项: %v", updatedKeys) + } return configs } diff --git a/db/dto/system_config.go b/db/dto/system_config.go index 8e86a5b..fde6c4c 100644 --- a/db/dto/system_config.go +++ b/db/dto/system_config.go @@ -3,38 +3,38 @@ package dto // SystemConfigRequest 系统配置请求 type SystemConfigRequest struct { // SEO 配置 - SiteTitle string `json:"site_title"` - SiteDescription string `json:"site_description"` - Keywords string `json:"keywords"` - Author string `json:"author"` - Copyright string `json:"copyright"` - SiteLogo string `json:"site_logo"` + SiteTitle *string `json:"site_title,omitempty"` + SiteDescription *string `json:"site_description,omitempty"` + Keywords *string `json:"keywords,omitempty"` + Author *string `json:"author,omitempty"` + Copyright *string `json:"copyright,omitempty"` + SiteLogo *string `json:"site_logo,omitempty"` // 自动处理配置组 - AutoProcessReadyResources bool `json:"auto_process_ready_resources"` // 自动处理待处理资源 - AutoProcessInterval int `json:"auto_process_interval"` // 自动处理间隔(分钟) - AutoTransferEnabled bool `json:"auto_transfer_enabled"` // 开启自动转存 - AutoTransferLimitDays int `json:"auto_transfer_limit_days"` // 自动转存限制天数(0表示不限制) - AutoTransferMinSpace int `json:"auto_transfer_min_space"` // 最小存储空间(GB) - AutoFetchHotDramaEnabled bool `json:"auto_fetch_hot_drama_enabled"` // 自动拉取热播剧名字 + AutoProcessReadyResources *bool `json:"auto_process_ready_resources,omitempty"` // 自动处理待处理资源 + AutoProcessInterval *int `json:"auto_process_interval,omitempty"` // 自动处理间隔(分钟) + AutoTransferEnabled *bool `json:"auto_transfer_enabled,omitempty"` // 开启自动转存 + AutoTransferLimitDays *int `json:"auto_transfer_limit_days,omitempty"` // 自动转存限制天数(0表示不限制) + AutoTransferMinSpace *int `json:"auto_transfer_min_space,omitempty"` // 最小存储空间(GB) + AutoFetchHotDramaEnabled *bool `json:"auto_fetch_hot_drama_enabled,omitempty"` // 自动拉取热播剧名字 // API配置 - ApiToken string `json:"api_token"` // 公开API访问令牌 + ApiToken *string `json:"api_token,omitempty"` // 公开API访问令牌 // 违禁词配置 - ForbiddenWords string `json:"forbidden_words"` // 违禁词列表,用逗号分隔 + ForbiddenWords *string `json:"forbidden_words,omitempty"` // 违禁词列表,用逗号分隔 // 广告配置 - AdKeywords string `json:"ad_keywords"` // 广告关键词列表,用逗号分隔 - AutoInsertAd string `json:"auto_insert_ad"` // 自动插入广告内容 + AdKeywords *string `json:"ad_keywords,omitempty"` // 广告关键词列表,用逗号分隔 + AutoInsertAd *string `json:"auto_insert_ad,omitempty"` // 自动插入广告内容 // 其他配置 - PageSize int `json:"page_size"` - MaintenanceMode bool `json:"maintenance_mode"` - EnableRegister bool `json:"enable_register"` // 开启注册功能 + PageSize *int `json:"page_size,omitempty"` + MaintenanceMode *bool `json:"maintenance_mode,omitempty"` + EnableRegister *bool `json:"enable_register,omitempty"` // 开启注册功能 // 三方统计配置 - ThirdPartyStatsCode string `json:"third_party_stats_code"` // 三方统计代码 + ThirdPartyStatsCode *string `json:"third_party_stats_code,omitempty"` // 三方统计代码 } // SystemConfigResponse 系统配置响应 @@ -66,7 +66,7 @@ type SystemConfigResponse struct { ForbiddenWords string `json:"forbidden_words"` // 违禁词列表,用逗号分隔 // 广告配置 - AdKeywords string `json:"ad_keywords"` // 广告关键词列表,用逗号分隔 + AdKeywords string `json:"ad_keywords"` // 广告关键词列表,用逗号分隔 AutoInsertAd string `json:"auto_insert_ad"` // 自动插入广告内容 // 其他配置 diff --git a/db/repo/system_config_repository.go b/db/repo/system_config_repository.go index 0cdd605..287ad7b 100644 --- a/db/repo/system_config_repository.go +++ b/db/repo/system_config_repository.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/ctwj/urldb/db/entity" + "github.com/ctwj/urldb/utils" "gorm.io/gorm" ) @@ -21,6 +22,8 @@ type SystemConfigRepository interface { GetConfigInt(key string) (int, error) GetCachedConfigs() map[string]string ClearConfigCache() + SafeRefreshConfigCache() error + ValidateConfigIntegrity() error } // SystemConfigRepositoryImpl 系统配置Repository实现 @@ -60,27 +63,39 @@ func (r *SystemConfigRepositoryImpl) FindByKey(key string) (*entity.SystemConfig // UpsertConfigs 批量创建或更新配置 func (r *SystemConfigRepositoryImpl) UpsertConfigs(configs []entity.SystemConfig) error { - for _, config := range configs { - var existingConfig entity.SystemConfig - err := r.db.Where("key = ?", config.Key).First(&existingConfig).Error + // 使用事务确保数据一致性 + return r.db.Transaction(func(tx *gorm.DB) error { + // 在更新前备份当前配置 + var existingConfigs []entity.SystemConfig + if err := tx.Find(&existingConfigs).Error; err != nil { + utils.Error("备份配置失败: %v", err) + // 不返回错误,继续执行更新 + } - if err != nil { - // 如果不存在,则创建 - if err := r.db.Create(&config).Error; err != nil { - return err - } - } else { - // 如果存在,则更新 - config.ID = existingConfig.ID - if err := r.db.Save(&config).Error; err != nil { - return err + for _, config := range configs { + var existingConfig entity.SystemConfig + err := tx.Where("key = ?", config.Key).First(&existingConfig).Error + + if err != nil { + // 如果不存在,则创建 + if err := tx.Create(&config).Error; err != nil { + utils.Error("创建配置失败 [%s]: %v", config.Key, err) + return fmt.Errorf("创建配置失败 [%s]: %v", config.Key, err) + } + } else { + // 如果存在,则更新 + config.ID = existingConfig.ID + if err := tx.Save(&config).Error; err != nil { + utils.Error("更新配置失败 [%s]: %v", config.Key, err) + return fmt.Errorf("更新配置失败 [%s]: %v", config.Key, err) + } } } - } - // 更新配置后刷新缓存 - r.refreshConfigCache() - return nil + // 更新成功后刷新缓存 + r.refreshConfigCache() + return nil + }) } // GetOrCreateDefault 获取配置或创建默认配置 @@ -92,6 +107,7 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig // 如果没有配置,创建默认配置 if len(configs) == 0 { + utils.Info("未找到任何配置,创建默认配置") defaultConfigs := []entity.SystemConfig{ {Key: entity.ConfigKeySiteTitle, Value: entity.ConfigDefaultSiteTitle, Type: entity.ConfigTypeString}, {Key: entity.ConfigKeySiteDescription, Value: entity.ConfigDefaultSiteDescription, Type: entity.ConfigTypeString}, @@ -105,10 +121,10 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig {Key: entity.ConfigKeyAutoTransferMinSpace, Value: entity.ConfigDefaultAutoTransferMinSpace, Type: entity.ConfigTypeInt}, {Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: entity.ConfigDefaultAutoFetchHotDramaEnabled, Type: entity.ConfigTypeBool}, {Key: entity.ConfigKeyApiToken, Value: entity.ConfigDefaultApiToken, Type: entity.ConfigTypeString}, - {Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt}, - {Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool}, - {Key: entity.ConfigKeyEnableRegister, Value: entity.ConfigDefaultEnableRegister, Type: entity.ConfigTypeBool}, - {Key: entity.ConfigKeyThirdPartyStatsCode, Value: entity.ConfigDefaultThirdPartyStatsCode, Type: entity.ConfigTypeString}, + {Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt}, + {Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool}, + {Key: entity.ConfigKeyEnableRegister, Value: entity.ConfigDefaultEnableRegister, Type: entity.ConfigTypeBool}, + {Key: entity.ConfigKeyThirdPartyStatsCode, Value: entity.ConfigDefaultThirdPartyStatsCode, Type: entity.ConfigTypeString}, } err = r.UpsertConfigs(defaultConfigs) @@ -208,6 +224,66 @@ func (r *SystemConfigRepositoryImpl) refreshConfigCache() { r.initConfigCache() } +// SafeRefreshConfigCache 安全的刷新配置缓存(带错误处理) +func (r *SystemConfigRepositoryImpl) SafeRefreshConfigCache() error { + defer func() { + if r := recover(); r != nil { + utils.Error("配置缓存刷新时发生panic: %v", r) + } + }() + + r.refreshConfigCache() + return nil +} + +// ValidateConfigIntegrity 验证配置完整性 +func (r *SystemConfigRepositoryImpl) ValidateConfigIntegrity() error { + configs, err := r.FindAll() + if err != nil { + return fmt.Errorf("获取配置失败: %v", err) + } + + // 检查关键配置是否存在 + requiredKeys := []string{ + entity.ConfigKeySiteTitle, + entity.ConfigKeySiteDescription, + entity.ConfigKeyKeywords, + entity.ConfigKeyAuthor, + entity.ConfigKeyCopyright, + entity.ConfigKeyAutoProcessReadyResources, + entity.ConfigKeyAutoProcessInterval, + entity.ConfigKeyAutoTransferEnabled, + entity.ConfigKeyAutoTransferLimitDays, + entity.ConfigKeyAutoTransferMinSpace, + entity.ConfigKeyAutoFetchHotDramaEnabled, + entity.ConfigKeyApiToken, + entity.ConfigKeyPageSize, + entity.ConfigKeyMaintenanceMode, + entity.ConfigKeyEnableRegister, + entity.ConfigKeyThirdPartyStatsCode, + } + + existingKeys := make(map[string]bool) + for _, config := range configs { + existingKeys[config.Key] = true + } + + var missingKeys []string + for _, key := range requiredKeys { + if !existingKeys[key] { + missingKeys = append(missingKeys, key) + } + } + + if len(missingKeys) > 0 { + utils.Error("发现缺失的配置项: %v", missingKeys) + return fmt.Errorf("配置不完整,缺失: %v", missingKeys) + } + + utils.Info("配置完整性检查通过") + return nil +} + // GetConfigValue 获取配置值(字符串) func (r *SystemConfigRepositoryImpl) GetConfigValue(key string) (string, error) { // 初始化缓存 diff --git a/handlers/system_config_handler.go b/handlers/system_config_handler.go index 50381ee..755ae2a 100644 --- a/handlers/system_config_handler.go +++ b/handlers/system_config_handler.go @@ -28,6 +28,20 @@ func NewSystemConfigHandler(systemConfigRepo repo.SystemConfigRepository) *Syste // GetConfig 获取系统配置 func (h *SystemConfigHandler) GetConfig(c *gin.Context) { + // 先验证配置完整性 + if err := h.systemConfigRepo.ValidateConfigIntegrity(); err != nil { + utils.Error("配置完整性检查失败: %v", err) + // 如果配置不完整,尝试重新创建默认配置 + configs, err := h.systemConfigRepo.GetOrCreateDefault() + if err != nil { + ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError) + return + } + configResponse := converter.SystemConfigToResponse(configs) + SuccessResponse(c, configResponse) + return + } + configs, err := h.systemConfigRepo.GetOrCreateDefault() if err != nil { ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError) @@ -47,22 +61,22 @@ func (h *SystemConfigHandler) UpdateConfig(c *gin.Context) { } // 验证参数 - 只验证提交的字段 - if req.SiteTitle != "" && (len(req.SiteTitle) < 1 || len(req.SiteTitle) > 100) { + if req.SiteTitle != nil && (len(*req.SiteTitle) < 1 || len(*req.SiteTitle) > 100) { ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest) return } - if req.AutoProcessInterval > 0 && (req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440) { + if req.AutoProcessInterval != nil && (*req.AutoProcessInterval < 1 || *req.AutoProcessInterval > 1440) { ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest) return } - if req.PageSize > 0 && (req.PageSize < 10 || req.PageSize > 500) { + if req.PageSize != nil && (*req.PageSize < 10 || *req.PageSize > 500) { ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest) return } - if req.AutoTransferMinSpace > 0 && (req.AutoTransferMinSpace < 100 || req.AutoTransferMinSpace > 1024) { + if req.AutoTransferMinSpace != nil && (*req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024) { ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest) return } @@ -118,29 +132,37 @@ func UpdateSystemConfig(c *gin.Context) { // 调试信息 utils.Info("接收到的配置请求: %+v", req) + // 获取当前配置作为备份 + currentConfigs, err := repoManager.SystemConfigRepository.FindAll() + if err != nil { + utils.Error("获取当前配置失败: %v", err) + } else { + utils.Info("当前配置数量: %d", len(currentConfigs)) + } + // 验证参数 - 只验证提交的字段 - if req.SiteTitle != "" && (len(req.SiteTitle) < 1 || len(req.SiteTitle) > 100) { + if req.SiteTitle != nil && (len(*req.SiteTitle) < 1 || len(*req.SiteTitle) > 100) { ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest) return } - if req.AutoProcessInterval != 0 && (req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440) { + if req.AutoProcessInterval != nil && (*req.AutoProcessInterval < 1 || *req.AutoProcessInterval > 1440) { ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest) return } - if req.PageSize != 0 && (req.PageSize < 10 || req.PageSize > 500) { + if req.PageSize != nil && (*req.PageSize < 10 || *req.PageSize > 500) { ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest) return } // 验证自动转存配置 - if req.AutoTransferLimitDays != 0 && (req.AutoTransferLimitDays < 0 || req.AutoTransferLimitDays > 365) { + if req.AutoTransferLimitDays != nil && (*req.AutoTransferLimitDays < 0 || *req.AutoTransferLimitDays > 365) { ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest) return } - if req.AutoTransferMinSpace != 0 && (req.AutoTransferMinSpace < 100 || req.AutoTransferMinSpace > 1024) { + if req.AutoTransferMinSpace != nil && (*req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024) { ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest) return } @@ -152,13 +174,24 @@ func UpdateSystemConfig(c *gin.Context) { return } + utils.Info("准备更新配置,配置项数量: %d", len(configs)) + // 保存配置 - err := repoManager.SystemConfigRepository.UpsertConfigs(configs) + err = repoManager.SystemConfigRepository.UpsertConfigs(configs) if err != nil { + utils.Error("保存系统配置失败: %v", err) ErrorResponse(c, "保存系统配置失败", http.StatusInternalServerError) return } + utils.Info("配置保存成功") + + // 安全刷新系统配置缓存 + if err := repoManager.SystemConfigRepository.SafeRefreshConfigCache(); err != nil { + utils.Error("刷新配置缓存失败: %v", err) + // 不返回错误,因为配置已经保存成功 + } + // 刷新系统配置缓存 pan.RefreshSystemConfigCache() @@ -174,16 +207,30 @@ func UpdateSystemConfig(c *gin.Context) { repoManager.CategoryRepository, ) if scheduler != nil { - scheduler.UpdateSchedulerStatusWithAutoTransfer(req.AutoFetchHotDramaEnabled, req.AutoProcessReadyResources, req.AutoTransferEnabled) + // 只更新被设置的配置 + var autoFetchHotDrama, autoProcessReady, autoTransfer bool + if req.AutoFetchHotDramaEnabled != nil { + autoFetchHotDrama = *req.AutoFetchHotDramaEnabled + } + if req.AutoProcessReadyResources != nil { + autoProcessReady = *req.AutoProcessReadyResources + } + if req.AutoTransferEnabled != nil { + autoTransfer = *req.AutoTransferEnabled + } + scheduler.UpdateSchedulerStatusWithAutoTransfer(autoFetchHotDrama, autoProcessReady, autoTransfer) } // 返回更新后的配置 updatedConfigs, err := repoManager.SystemConfigRepository.FindAll() if err != nil { + utils.Error("获取更新后的配置失败: %v", err) ErrorResponse(c, "获取更新后的配置失败", http.StatusInternalServerError) return } + utils.Info("配置更新完成,当前配置数量: %d", len(updatedConfigs)) + configResponse := converter.SystemConfigToResponse(updatedConfigs) SuccessResponse(c, configResponse) } @@ -199,6 +246,36 @@ func GetPublicSystemConfig(c *gin.Context) { SuccessResponse(c, configResponse) } +// 新增:配置监控端点 +func GetConfigStatus(c *gin.Context) { + // 获取配置统计信息 + configs, err := repoManager.SystemConfigRepository.FindAll() + if err != nil { + ErrorResponse(c, "获取配置状态失败", http.StatusInternalServerError) + return + } + + // 验证配置完整性 + integrityErr := repoManager.SystemConfigRepository.ValidateConfigIntegrity() + + // 获取缓存状态 + cachedConfigs := repoManager.SystemConfigRepository.GetCachedConfigs() + + status := map[string]interface{}{ + "total_configs": len(configs), + "cached_configs": len(cachedConfigs), + "integrity_check": integrityErr == nil, + "integrity_error": "", + "last_check_time": utils.GetCurrentTimeString(), + } + + if integrityErr != nil { + status["integrity_error"] = integrityErr.Error() + } + + SuccessResponse(c, status) +} + // 新增:切换自动处理配置 func ToggleAutoProcess(c *gin.Context) { var req struct { diff --git a/main.go b/main.go index ddde72a..445d567 100644 --- a/main.go +++ b/main.go @@ -215,6 +215,7 @@ func main() { // 系统配置路由 api.GET("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemConfig) api.POST("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateSystemConfig) + api.GET("/system/config/status", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetConfigStatus) api.POST("/system/config/toggle-auto-process", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ToggleAutoProcess) api.GET("/public/system-config", handlers.GetPublicSystemConfig) diff --git a/web/components.d.ts b/web/components.d.ts index 37a76e1..f5e5b5a 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -41,6 +41,7 @@ declare module 'vue' { NSpace: typeof import('naive-ui')['NSpace'] NSpin: typeof import('naive-ui')['NSpin'] NSwitch: typeof import('naive-ui')['NSwitch'] + NTable: typeof import('naive-ui')['NTable'] NTabPane: typeof import('naive-ui')['NTabPane'] NTabs: typeof import('naive-ui')['NTabs'] NTag: typeof import('naive-ui')['NTag'] diff --git a/web/composables/useApi.ts b/web/composables/useApi.ts index e3d66f9..d4ef94b 100644 --- a/web/composables/useApi.ts +++ b/web/composables/useApi.ts @@ -170,8 +170,9 @@ export const useSearchStatsApi = () => { export const useSystemConfigApi = () => { const getSystemConfig = () => useApiFetch('/system/config').then(parseApiResponse) const updateSystemConfig = (data: any) => useApiFetch('/system/config', { method: 'POST', body: data }).then(parseApiResponse) + const getConfigStatus = () => useApiFetch('/system/config/status').then(parseApiResponse) const toggleAutoProcess = (enabled: boolean) => useApiFetch('/system/config/toggle-auto-process', { method: 'POST', body: { auto_process_ready_resources: enabled } }).then(parseApiResponse) - return { getSystemConfig, updateSystemConfig, toggleAutoProcess } + return { getSystemConfig, updateSystemConfig, getConfigStatus, toggleAutoProcess } } export const useHotDramaApi = () => { diff --git a/web/composables/useConfigChangeDetection.ts b/web/composables/useConfigChangeDetection.ts new file mode 100644 index 0000000..b2781eb --- /dev/null +++ b/web/composables/useConfigChangeDetection.ts @@ -0,0 +1,307 @@ +import { ref } from 'vue' +import type { Ref } from 'vue' + +export interface ConfigChangeDetectionOptions { + // 是否启用自动检测 + autoDetect?: boolean + // 是否在控制台输出调试信息 + debug?: boolean + // 自定义比较函数 + customCompare?: (key: string, currentValue: any, originalValue: any) => boolean + // 配置项映射(前端字段名 -> 后端字段名) + fieldMapping?: Record +} + +export interface ConfigSubmitOptions { + // 是否只提交改动的字段 + onlyChanged?: boolean + // 是否包含所有配置项(用于后端识别) + includeAllFields?: boolean + // 自定义提交数据转换 + transformSubmitData?: (data: any) => any +} + +export const useConfigChangeDetection = >( + options: ConfigChangeDetectionOptions = {} +) => { + const { autoDetect = true, debug = false, customCompare, fieldMapping = {} } = options + + // 原始配置数据 + const originalConfig = ref({} as T) + + // 当前配置数据 + const currentConfig = ref({} as T) + + // 是否已初始化 + const isInitialized = ref(false) + + /** + * 设置原始配置数据 + */ + const setOriginalConfig = (config: T) => { + originalConfig.value = { ...config } + currentConfig.value = { ...config } + isInitialized.value = true + + if (debug) { + console.log('useConfigChangeDetection - 设置原始配置:', config) + } + } + + /** + * 更新当前配置数据 + */ + const updateCurrentConfig = (config: Partial) => { + currentConfig.value = { ...currentConfig.value, ...config } + + if (debug) { + console.log('useConfigChangeDetection - 更新当前配置:', config) + } + } + + /** + * 检测配置改动 + */ + const getChangedConfig = (): Partial => { + if (!isInitialized.value) { + if (debug) { + console.warn('useConfigChangeDetection - 配置未初始化') + } + return {} + } + + const changedConfig: Partial = {} + + // 遍历所有配置项 + for (const key in currentConfig.value) { + const currentValue = currentConfig.value[key] + const originalValue = originalConfig.value[key] + + // 使用自定义比较函数或默认比较 + const hasChanged = customCompare + ? customCompare(key, currentValue, originalValue) + : currentValue !== originalValue + + if (hasChanged) { + changedConfig[key as keyof T] = currentValue + } + } + + if (debug) { + console.log('useConfigChangeDetection - 检测到的改动:', changedConfig) + } + + return changedConfig + } + + /** + * 检查是否有改动 + */ + const hasChanges = (): boolean => { + const changedConfig = getChangedConfig() + return Object.keys(changedConfig).length > 0 + } + + /** + * 获取改动的字段列表 + */ + const getChangedFields = (): string[] => { + const changedConfig = getChangedConfig() + return Object.keys(changedConfig) + } + + /** + * 获取改动的详细信息 + */ + const getChangedDetails = (): Array<{ + key: string + originalValue: any + currentValue: any + }> => { + if (!isInitialized.value) { + return [] + } + + const details: Array<{ + key: string + originalValue: any + currentValue: any + }> = [] + + for (const key in currentConfig.value) { + const currentValue = currentConfig.value[key] + const originalValue = originalConfig.value[key] + + const hasChanged = customCompare + ? customCompare(key, currentValue, originalValue) + : currentValue !== originalValue + + if (hasChanged) { + details.push({ + key, + originalValue, + currentValue + }) + } + } + + return details + } + + /** + * 重置为原始配置 + */ + const resetToOriginal = () => { + currentConfig.value = { ...originalConfig.value } + + if (debug) { + console.log('useConfigChangeDetection - 重置为原始配置') + } + } + + /** + * 更新原始配置(通常在保存成功后调用) + */ + const updateOriginalConfig = () => { + originalConfig.value = { ...currentConfig.value } + + if (debug) { + console.log('useConfigChangeDetection - 更新原始配置') + } + } + + /** + * 获取配置快照 + */ + const getSnapshot = () => { + return { + original: { ...originalConfig.value }, + current: { ...currentConfig.value }, + changed: getChangedConfig(), + hasChanges: hasChanges() + } + } + + /** + * 准备提交数据 + */ + const prepareSubmitData = (submitOptions: ConfigSubmitOptions = {}): any => { + const { onlyChanged = true, includeAllFields = true, transformSubmitData } = submitOptions + + let submitData: any = {} + + if (onlyChanged) { + // 只提交改动的字段 + submitData = getChangedConfig() + } else { + // 提交所有字段 + submitData = { ...currentConfig.value } + } + + // 应用字段映射 + if (Object.keys(fieldMapping).length > 0) { + const mappedData: any = {} + for (const [frontendKey, backendKey] of Object.entries(fieldMapping)) { + if (submitData[frontendKey] !== undefined) { + mappedData[backendKey] = submitData[frontendKey] + } + } + submitData = mappedData + } + + // 如果包含所有字段,添加未改动的字段(值为undefined,让后端知道这些字段存在但未改动) + if (includeAllFields && onlyChanged) { + for (const key in originalConfig.value) { + if (submitData[key] === undefined) { + submitData[key] = undefined + } + } + } + + // 应用自定义转换 + if (transformSubmitData) { + submitData = transformSubmitData(submitData) + } + + if (debug) { + console.log('useConfigChangeDetection - 准备提交数据:', submitData) + } + + return submitData + } + + /** + * 通用配置保存函数 + */ + const saveConfig = async ( + apiFunction: (data: any) => Promise, + submitOptions: ConfigSubmitOptions = {}, + onSuccess?: () => void, + onError?: (error: any) => void + ) => { + try { + // 检测是否有改动 + if (!hasChanges()) { + if (debug) { + console.log('useConfigChangeDetection - 没有检测到改动,跳过保存') + } + return { success: true, message: '没有检测到任何改动' } + } + + // 准备提交数据 + const submitData = prepareSubmitData(submitOptions) + + if (debug) { + console.log('useConfigChangeDetection - 提交数据:', submitData) + } + + // 调用API + const response = await apiFunction(submitData) + + // 更新原始配置 + updateOriginalConfig() + + if (debug) { + console.log('useConfigChangeDetection - 保存成功') + } + + // 调用成功回调 + if (onSuccess) { + onSuccess() + } + + return { success: true, response } + } catch (error) { + if (debug) { + console.error('useConfigChangeDetection - 保存失败:', error) + } + + // 调用错误回调 + if (onError) { + onError(error) + } + + throw error + } + } + + return { + // 响应式数据 + originalConfig: originalConfig as Ref, + currentConfig: currentConfig as Ref, + isInitialized, + + // 方法 + setOriginalConfig, + updateCurrentConfig, + getChangedConfig, + hasChanges, + getChangedFields, + getChangedDetails, + resetToOriginal, + updateOriginalConfig, + getSnapshot, + prepareSubmitData, + saveConfig + } +} \ No newline at end of file diff --git a/web/pages/admin/bot.vue b/web/pages/admin/bot.vue index 7ef090a..345b773 100644 --- a/web/pages/admin/bot.vue +++ b/web/pages/admin/bot.vue @@ -189,12 +189,34 @@