mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
opt: 优化数据库连接池,配置管理,错误处理
This commit is contained in:
676
config/config.go
Normal file
676
config/config.go
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ctwj/urldb/db/entity"
|
||||||
|
"github.com/ctwj/urldb/db/repo"
|
||||||
|
"github.com/ctwj/urldb/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigManager 统一配置管理器
|
||||||
|
type ConfigManager struct {
|
||||||
|
repo *repo.RepositoryManager
|
||||||
|
|
||||||
|
// 内存缓存
|
||||||
|
cache map[string]*ConfigItem
|
||||||
|
cacheMutex sync.RWMutex
|
||||||
|
cacheOnce sync.Once
|
||||||
|
|
||||||
|
// 配置更新通知
|
||||||
|
configUpdateCh chan string
|
||||||
|
watchers []chan string
|
||||||
|
watcherMutex sync.Mutex
|
||||||
|
|
||||||
|
// 加载时间
|
||||||
|
lastLoadTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigItem 配置项结构
|
||||||
|
type ConfigItem struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Group string `json:"group"` // 配置分组
|
||||||
|
Category string `json:"category"` // 配置分类
|
||||||
|
IsSensitive bool `json:"is_sensitive"` // 是否是敏感信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigGroup 配置分组
|
||||||
|
type ConfigGroup string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GroupDatabase ConfigGroup = "database"
|
||||||
|
GroupServer ConfigGroup = "server"
|
||||||
|
GroupSecurity ConfigGroup = "security"
|
||||||
|
GroupSearch ConfigGroup = "search"
|
||||||
|
GroupTelegram ConfigGroup = "telegram"
|
||||||
|
GroupCache ConfigGroup = "cache"
|
||||||
|
GroupMeilisearch ConfigGroup = "meilisearch"
|
||||||
|
GroupSEO ConfigGroup = "seo"
|
||||||
|
GroupAutoProcess ConfigGroup = "auto_process"
|
||||||
|
GroupOther ConfigGroup = "other"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewConfigManager 创建配置管理器
|
||||||
|
func NewConfigManager(repoManager *repo.RepositoryManager) *ConfigManager {
|
||||||
|
cm := &ConfigManager{
|
||||||
|
repo: repoManager,
|
||||||
|
cache: make(map[string]*ConfigItem),
|
||||||
|
configUpdateCh: make(chan string, 100), // 缓冲通道防止阻塞
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动配置更新监听器
|
||||||
|
go cm.startConfigUpdateListener()
|
||||||
|
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// startConfigUpdateListener 启动配置更新监听器
|
||||||
|
func (cm *ConfigManager) startConfigUpdateListener() {
|
||||||
|
for key := range cm.configUpdateCh {
|
||||||
|
cm.notifyWatchers(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyWatchers 通知所有监听器配置已更新
|
||||||
|
func (cm *ConfigManager) notifyWatchers(key string) {
|
||||||
|
cm.watcherMutex.Lock()
|
||||||
|
defer cm.watcherMutex.Unlock()
|
||||||
|
|
||||||
|
for _, watcher := range cm.watchers {
|
||||||
|
select {
|
||||||
|
case watcher <- key:
|
||||||
|
default:
|
||||||
|
// 如果通道阻塞,跳过该监听器
|
||||||
|
utils.Warn("配置监听器通道阻塞,跳过通知: %s", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddConfigWatcher 添加配置变更监听器
|
||||||
|
func (cm *ConfigManager) AddConfigWatcher() chan string {
|
||||||
|
cm.watcherMutex.Lock()
|
||||||
|
defer cm.watcherMutex.Unlock()
|
||||||
|
|
||||||
|
watcher := make(chan string, 10) // 为每个监听器创建缓冲通道
|
||||||
|
cm.watchers = append(cm.watchers, watcher)
|
||||||
|
return watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig 获取配置项
|
||||||
|
func (cm *ConfigManager) GetConfig(key string) (*ConfigItem, error) {
|
||||||
|
// 先尝试从内存缓存获取
|
||||||
|
item, exists := cm.getCachedConfig(key)
|
||||||
|
if exists {
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果缓存中没有,从数据库获取
|
||||||
|
config, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将数据库配置转换为ConfigItem并缓存
|
||||||
|
item = &ConfigItem{
|
||||||
|
Key: config.Key,
|
||||||
|
Value: config.Value,
|
||||||
|
Type: config.Type,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||||
|
item.Group = string(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||||
|
item.Category = category
|
||||||
|
}
|
||||||
|
|
||||||
|
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||||
|
|
||||||
|
// 缓存配置
|
||||||
|
cm.setCachedConfig(key, item)
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigValue 获取配置值
|
||||||
|
func (cm *ConfigManager) GetConfigValue(key string) (string, error) {
|
||||||
|
item, err := cm.GetConfig(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return item.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigBool 获取布尔值配置
|
||||||
|
func (cm *ConfigManager) GetConfigBool(key string) (bool, error) {
|
||||||
|
value, err := cm.GetConfigValue(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(value) {
|
||||||
|
case "true", "1", "yes", "on":
|
||||||
|
return true, nil
|
||||||
|
case "false", "0", "no", "off", "":
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("无法将配置值 '%s' 转换为布尔值", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigInt 获取整数值配置
|
||||||
|
func (cm *ConfigManager) GetConfigInt(key string) (int, error) {
|
||||||
|
value, err := cm.GetConfigValue(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigInt64 获取64位整数值配置
|
||||||
|
func (cm *ConfigManager) GetConfigInt64(key string) (int64, error) {
|
||||||
|
value, err := cm.GetConfigValue(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.ParseInt(value, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigFloat64 获取浮点数配置
|
||||||
|
func (cm *ConfigManager) GetConfigFloat64(key string) (float64, error) {
|
||||||
|
value, err := cm.GetConfigValue(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.ParseFloat(value, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig 设置配置值
|
||||||
|
func (cm *ConfigManager) SetConfig(key, value string) error {
|
||||||
|
// 更新数据库
|
||||||
|
config := &entity.SystemConfig{
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
Type: "string", // 默认类型,实际类型应该从现有配置中获取
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取现有配置以确定类型
|
||||||
|
existing, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||||
|
if err == nil {
|
||||||
|
config.Type = existing.Type
|
||||||
|
} else {
|
||||||
|
// 如果配置不存在,尝试从默认配置中获取类型
|
||||||
|
config.Type = cm.getDefaultConfigType(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
err = cm.repo.SystemConfigRepository.UpsertConfigs([]entity.SystemConfig{*config})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("保存配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
item := &ConfigItem{
|
||||||
|
Key: config.Key,
|
||||||
|
Value: config.Value,
|
||||||
|
Type: config.Type,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||||
|
item.Group = string(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||||
|
item.Category = category
|
||||||
|
}
|
||||||
|
|
||||||
|
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||||
|
|
||||||
|
cm.setCachedConfig(key, item)
|
||||||
|
|
||||||
|
// 发送更新通知
|
||||||
|
cm.configUpdateCh <- key
|
||||||
|
|
||||||
|
utils.Info("配置已更新: %s = %s", key, value)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfigWithType 设置配置值(指定类型)
|
||||||
|
func (cm *ConfigManager) SetConfigWithType(key, value, configType string) error {
|
||||||
|
config := &entity.SystemConfig{
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
Type: configType,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cm.repo.SystemConfigRepository.UpsertConfigs([]entity.SystemConfig{*config})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("保存配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
item := &ConfigItem{
|
||||||
|
Key: config.Key,
|
||||||
|
Value: config.Value,
|
||||||
|
Type: config.Type,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||||
|
item.Group = string(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||||
|
item.Category = category
|
||||||
|
}
|
||||||
|
|
||||||
|
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||||
|
|
||||||
|
cm.setCachedConfig(key, item)
|
||||||
|
|
||||||
|
// 发送更新通知
|
||||||
|
cm.configUpdateCh <- key
|
||||||
|
|
||||||
|
utils.Info("配置已更新: %s = %s (type: %s)", key, value, configType)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGroupByConfigKey 根据配置键获取分组
|
||||||
|
func (cm *ConfigManager) getGroupByConfigKey(key string) ConfigGroup {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(key, "database_"), strings.HasPrefix(key, "db_"):
|
||||||
|
return GroupDatabase
|
||||||
|
case strings.HasPrefix(key, "server_"), strings.HasPrefix(key, "port"), strings.HasPrefix(key, "host"):
|
||||||
|
return GroupServer
|
||||||
|
case strings.HasPrefix(key, "api_"), strings.HasPrefix(key, "jwt_"), strings.HasPrefix(key, "password"):
|
||||||
|
return GroupSecurity
|
||||||
|
case strings.Contains(key, "meilisearch"):
|
||||||
|
return GroupMeilisearch
|
||||||
|
case strings.Contains(key, "telegram"):
|
||||||
|
return GroupTelegram
|
||||||
|
case strings.Contains(key, "cache"), strings.Contains(key, "redis"):
|
||||||
|
return GroupCache
|
||||||
|
case strings.Contains(key, "seo"), strings.Contains(key, "title"), strings.Contains(key, "keyword"):
|
||||||
|
return GroupSEO
|
||||||
|
case strings.Contains(key, "auto_"):
|
||||||
|
return GroupAutoProcess
|
||||||
|
case strings.Contains(key, "forbidden"), strings.Contains(key, "ad_"):
|
||||||
|
return GroupOther
|
||||||
|
default:
|
||||||
|
return GroupOther
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCategoryByConfigKey 根据配置键获取分类
|
||||||
|
func (cm *ConfigManager) getCategoryByConfigKey(key string) string {
|
||||||
|
switch {
|
||||||
|
case key == entity.ConfigKeySiteTitle || key == entity.ConfigKeySiteDescription:
|
||||||
|
return "basic_info"
|
||||||
|
case key == entity.ConfigKeyKeywords || key == entity.ConfigKeyAuthor:
|
||||||
|
return "seo"
|
||||||
|
case key == entity.ConfigKeyAutoProcessReadyResources || key == entity.ConfigKeyAutoProcessInterval:
|
||||||
|
return "auto_process"
|
||||||
|
case key == entity.ConfigKeyAutoTransferEnabled || key == entity.ConfigKeyAutoTransferLimitDays:
|
||||||
|
return "auto_transfer"
|
||||||
|
case key == entity.ConfigKeyMeilisearchEnabled || key == entity.ConfigKeyMeilisearchHost:
|
||||||
|
return "search"
|
||||||
|
case key == entity.ConfigKeyTelegramBotEnabled || key == entity.ConfigKeyTelegramBotApiKey:
|
||||||
|
return "telegram"
|
||||||
|
case key == entity.ConfigKeyMaintenanceMode || key == entity.ConfigKeyEnableRegister:
|
||||||
|
return "system"
|
||||||
|
case key == entity.ConfigKeyForbiddenWords || key == entity.ConfigKeyAdKeywords:
|
||||||
|
return "filtering"
|
||||||
|
default:
|
||||||
|
return "other"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSensitiveConfig 判断是否是敏感配置
|
||||||
|
func (cm *ConfigManager) isSensitiveConfig(key string) bool {
|
||||||
|
switch key {
|
||||||
|
case entity.ConfigKeyApiToken,
|
||||||
|
entity.ConfigKeyMeilisearchMasterKey,
|
||||||
|
entity.ConfigKeyTelegramBotApiKey,
|
||||||
|
entity.ConfigKeyTelegramProxyUsername,
|
||||||
|
entity.ConfigKeyTelegramProxyPassword:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return strings.Contains(strings.ToLower(key), "password") ||
|
||||||
|
strings.Contains(strings.ToLower(key), "secret") ||
|
||||||
|
strings.Contains(strings.ToLower(key), "key") ||
|
||||||
|
strings.Contains(strings.ToLower(key), "token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultConfigType 获取默认配置类型
|
||||||
|
func (cm *ConfigManager) getDefaultConfigType(key string) string {
|
||||||
|
switch key {
|
||||||
|
case entity.ConfigKeyAutoProcessReadyResources,
|
||||||
|
entity.ConfigKeyAutoTransferEnabled,
|
||||||
|
entity.ConfigKeyAutoFetchHotDramaEnabled,
|
||||||
|
entity.ConfigKeyMaintenanceMode,
|
||||||
|
entity.ConfigKeyEnableRegister,
|
||||||
|
entity.ConfigKeyMeilisearchEnabled,
|
||||||
|
entity.ConfigKeyTelegramBotEnabled:
|
||||||
|
return entity.ConfigTypeBool
|
||||||
|
case entity.ConfigKeyAutoProcessInterval,
|
||||||
|
entity.ConfigKeyAutoTransferLimitDays,
|
||||||
|
entity.ConfigKeyAutoTransferMinSpace,
|
||||||
|
entity.ConfigKeyPageSize:
|
||||||
|
return entity.ConfigTypeInt
|
||||||
|
case entity.ConfigKeyAnnouncements:
|
||||||
|
return entity.ConfigTypeJSON
|
||||||
|
default:
|
||||||
|
return entity.ConfigTypeString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAllConfigs 加载所有配置到缓存
|
||||||
|
func (cm *ConfigManager) LoadAllConfigs() error {
|
||||||
|
configs, err := cm.repo.SystemConfigRepository.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("加载所有配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.cacheMutex.Lock()
|
||||||
|
defer cm.cacheMutex.Unlock()
|
||||||
|
|
||||||
|
// 清空现有缓存
|
||||||
|
cm.cache = make(map[string]*ConfigItem)
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
for _, config := range configs {
|
||||||
|
item := &ConfigItem{
|
||||||
|
Key: config.Key,
|
||||||
|
Value: config.Value,
|
||||||
|
Type: config.Type,
|
||||||
|
UpdatedAt: time.Now(), // 实际应该从数据库获取
|
||||||
|
}
|
||||||
|
|
||||||
|
if group := cm.getGroupByConfigKey(config.Key); group != "" {
|
||||||
|
item.Group = string(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
if category := cm.getCategoryByConfigKey(config.Key); category != "" {
|
||||||
|
item.Category = category
|
||||||
|
}
|
||||||
|
|
||||||
|
item.IsSensitive = cm.isSensitiveConfig(config.Key)
|
||||||
|
|
||||||
|
cm.cache[config.Key] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.lastLoadTime = time.Now()
|
||||||
|
|
||||||
|
utils.Info("已加载 %d 个配置项到缓存", len(configs))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshConfigCache 刷新配置缓存
|
||||||
|
func (cm *ConfigManager) RefreshConfigCache() error {
|
||||||
|
return cm.LoadAllConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCachedConfig 获取缓存的配置
|
||||||
|
func (cm *ConfigManager) getCachedConfig(key string) (*ConfigItem, bool) {
|
||||||
|
cm.cacheMutex.RLock()
|
||||||
|
defer cm.cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
item, exists := cm.cache[key]
|
||||||
|
return item, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCachedConfig 设置缓存的配置
|
||||||
|
func (cm *ConfigManager) setCachedConfig(key string, item *ConfigItem) {
|
||||||
|
cm.cacheMutex.Lock()
|
||||||
|
defer cm.cacheMutex.Unlock()
|
||||||
|
|
||||||
|
cm.cache[key] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigByGroup 按分组获取配置
|
||||||
|
func (cm *ConfigManager) GetConfigByGroup(group ConfigGroup) (map[string]*ConfigItem, error) {
|
||||||
|
cm.cacheMutex.RLock()
|
||||||
|
defer cm.cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
result := make(map[string]*ConfigItem)
|
||||||
|
|
||||||
|
for key, item := range cm.cache {
|
||||||
|
if ConfigGroup(item.Group) == group {
|
||||||
|
result[key] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigByCategory 按分类获取配置
|
||||||
|
func (cm *ConfigManager) GetConfigByCategory(category string) (map[string]*ConfigItem, error) {
|
||||||
|
cm.cacheMutex.RLock()
|
||||||
|
defer cm.cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
result := make(map[string]*ConfigItem)
|
||||||
|
|
||||||
|
for key, item := range cm.cache {
|
||||||
|
if item.Category == category {
|
||||||
|
result[key] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteConfig 删除配置
|
||||||
|
func (cm *ConfigManager) DeleteConfig(key string) error {
|
||||||
|
// 先查找配置获取ID
|
||||||
|
config, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查找配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据库删除
|
||||||
|
err = cm.repo.SystemConfigRepository.Delete(config.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("删除配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从缓存中移除
|
||||||
|
cm.cacheMutex.Lock()
|
||||||
|
delete(cm.cache, key)
|
||||||
|
cm.cacheMutex.Unlock()
|
||||||
|
|
||||||
|
utils.Info("配置已删除: %s", key)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSensitiveConfigKeys 获取所有敏感配置键
|
||||||
|
func (cm *ConfigManager) GetSensitiveConfigKeys() []string {
|
||||||
|
cm.cacheMutex.RLock()
|
||||||
|
defer cm.cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
var sensitiveKeys []string
|
||||||
|
for key, item := range cm.cache {
|
||||||
|
if item.IsSensitive {
|
||||||
|
sensitiveKeys = append(sensitiveKeys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sensitiveKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigWithMask 获取配置值(敏感配置会被遮蔽)
|
||||||
|
func (cm *ConfigManager) GetConfigWithMask(key string) (*ConfigItem, error) {
|
||||||
|
item, err := cm.GetConfig(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.IsSensitive {
|
||||||
|
// 创建副本并遮蔽敏感值
|
||||||
|
maskedItem := *item
|
||||||
|
maskedItem.Value = cm.maskSensitiveValue(item.Value)
|
||||||
|
return &maskedItem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskSensitiveValue 遮蔽敏感值
|
||||||
|
func (cm *ConfigManager) maskSensitiveValue(value string) string {
|
||||||
|
if len(value) <= 4 {
|
||||||
|
return "****"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留前2个和后2个字符,中间用****替代
|
||||||
|
return value[:2] + "****" + value[len(value)-2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigAsJSON 获取配置为JSON格式
|
||||||
|
func (cm *ConfigManager) GetConfigAsJSON() ([]byte, error) {
|
||||||
|
cm.cacheMutex.RLock()
|
||||||
|
defer cm.cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
// 创建副本,敏感配置使用遮蔽值
|
||||||
|
configMap := make(map[string]*ConfigItem)
|
||||||
|
for key, item := range cm.cache {
|
||||||
|
if item.IsSensitive {
|
||||||
|
maskedItem := *item
|
||||||
|
maskedItem.Value = cm.maskSensitiveValue(item.Value)
|
||||||
|
configMap[key] = &maskedItem
|
||||||
|
} else {
|
||||||
|
configMap[key] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.MarshalIndent(configMap, "", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigStatistics 获取配置统计信息
|
||||||
|
func (cm *ConfigManager) GetConfigStatistics() map[string]interface{} {
|
||||||
|
cm.cacheMutex.RLock()
|
||||||
|
defer cm.cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
stats := map[string]interface{}{
|
||||||
|
"total_configs": len(cm.cache),
|
||||||
|
"last_load_time": cm.lastLoadTime,
|
||||||
|
"cache_size_bytes": len(cm.cache) * 100, // 估算每个配置约100字节
|
||||||
|
"groups": make(map[string]int),
|
||||||
|
"types": make(map[string]int),
|
||||||
|
"categories": make(map[string]int),
|
||||||
|
"sensitive_configs": 0,
|
||||||
|
"config_keys": make([]string, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make(map[string]int)
|
||||||
|
types := make(map[string]int)
|
||||||
|
categories := make(map[string]int)
|
||||||
|
|
||||||
|
for key, item := range cm.cache {
|
||||||
|
// 统计分组
|
||||||
|
groups[item.Group]++
|
||||||
|
|
||||||
|
// 统计类型
|
||||||
|
types[item.Type]++
|
||||||
|
|
||||||
|
// 统计分类
|
||||||
|
categories[item.Category]++
|
||||||
|
|
||||||
|
// 统计敏感配置
|
||||||
|
if item.IsSensitive {
|
||||||
|
stats["sensitive_configs"] = stats["sensitive_configs"].(int) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加配置键到列表
|
||||||
|
keys := stats["config_keys"].([]string)
|
||||||
|
keys = append(keys, key)
|
||||||
|
stats["config_keys"] = keys
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["groups"] = groups
|
||||||
|
stats["types"] = types
|
||||||
|
stats["categories"] = categories
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvironmentConfig 从环境变量获取配置
|
||||||
|
func (cm *ConfigManager) GetEnvironmentConfig(key string) (string, bool) {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value != "" {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试使用大写版本的键
|
||||||
|
value = os.Getenv(strings.ToUpper(key))
|
||||||
|
if value != "" {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试使用大写带下划线的格式
|
||||||
|
upperKey := strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
|
||||||
|
value = os.Getenv(upperKey)
|
||||||
|
if value != "" {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigWithEnvFallback 获取配置,环境变量优先
|
||||||
|
func (cm *ConfigManager) GetConfigWithEnvFallback(configKey, envKey string) (string, error) {
|
||||||
|
// 优先从环境变量获取
|
||||||
|
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||||
|
return envValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果环境变量不存在,从数据库获取
|
||||||
|
return cm.GetConfigValue(configKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigIntWithEnvFallback 获取整数配置,环境变量优先
|
||||||
|
func (cm *ConfigManager) GetConfigIntWithEnvFallback(configKey, envKey string) (int, error) {
|
||||||
|
// 优先从环境变量获取
|
||||||
|
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||||
|
return strconv.Atoi(envValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果环境变量不存在,从数据库获取
|
||||||
|
return cm.GetConfigInt(configKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigBoolWithEnvFallback 获取布尔配置,环境变量优先
|
||||||
|
func (cm *ConfigManager) GetConfigBoolWithEnvFallback(configKey, envKey string) (bool, error) {
|
||||||
|
// 优先从环境变量获取
|
||||||
|
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||||
|
switch strings.ToLower(envValue) {
|
||||||
|
case "true", "1", "yes", "on":
|
||||||
|
return true, nil
|
||||||
|
case "false", "0", "no", "off", "":
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("无法将环境变量值 '%s' 转换为布尔值", envValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果环境变量不存在,从数据库获取
|
||||||
|
return cm.GetConfigBool(configKey)
|
||||||
|
}
|
||||||
124
config/global.go
Normal file
124
config/global.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalConfigManager *ConfigManager
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetGlobalConfigManager 设置全局配置管理器
|
||||||
|
func SetGlobalConfigManager(cm *ConfigManager) {
|
||||||
|
globalConfigManager = cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalConfigManager 获取全局配置管理器
|
||||||
|
func GetGlobalConfigManager() *ConfigManager {
|
||||||
|
return globalConfigManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig 获取配置值(全局函数)
|
||||||
|
func GetConfig(key string) (*ConfigItem, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return nil, ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfig(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigValue 获取配置值(全局函数)
|
||||||
|
func GetConfigValue(key string) (string, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return "", ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfigValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigBool 获取布尔配置值(全局函数)
|
||||||
|
func GetConfigBool(key string) (bool, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return false, ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfigBool(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigInt 获取整数配置值(全局函数)
|
||||||
|
func GetConfigInt(key string) (int, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return 0, ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfigInt(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigInt64 获取64位整数配置值(全局函数)
|
||||||
|
func GetConfigInt64(key string) (int64, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return 0, ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfigInt64(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigFloat64 获取浮点数配置值(全局函数)
|
||||||
|
func GetConfigFloat64(key string) (float64, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return 0, ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfigFloat64(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig 设置配置值(全局函数)
|
||||||
|
func SetConfig(key, value string) error {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.SetConfig(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfigWithType 设置配置值(指定类型,全局函数)
|
||||||
|
func SetConfigWithType(key, value, configType string) error {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.SetConfigWithType(key, value, configType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigWithEnvFallback 获取配置值(环境变量优先,全局函数)
|
||||||
|
func GetConfigWithEnvFallback(configKey, envKey string) (string, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return "", ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfigWithEnvFallback(configKey, envKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigIntWithEnvFallback 获取整数配置值(环境变量优先,全局函数)
|
||||||
|
func GetConfigIntWithEnvFallback(configKey, envKey string) (int, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return 0, ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfigIntWithEnvFallback(configKey, envKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigBoolWithEnvFallback 获取布尔配置值(环境变量优先,全局函数)
|
||||||
|
func GetConfigBoolWithEnvFallback(configKey, envKey string) (bool, error) {
|
||||||
|
if globalConfigManager == nil {
|
||||||
|
return false, ErrConfigManagerNotInitialized
|
||||||
|
}
|
||||||
|
return globalConfigManager.GetConfigBoolWithEnvFallback(configKey, envKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrConfigManagerNotInitialized 配置管理器未初始化错误
|
||||||
|
var ErrConfigManagerNotInitialized = &ConfigError{
|
||||||
|
Code: "CONFIG_MANAGER_NOT_INITIALIZED",
|
||||||
|
Message: "配置管理器未初始化",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigError 配置错误
|
||||||
|
type ConfigError struct {
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConfigError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
31
config/sync.go
Normal file
31
config/sync.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ctwj/urldb/db/repo"
|
||||||
|
"github.com/ctwj/urldb/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncWithRepository 同步配置管理器与Repository的缓存
|
||||||
|
func (cm *ConfigManager) SyncWithRepository(repoManager *repo.RepositoryManager) {
|
||||||
|
// 监听配置变更事件并同步缓存
|
||||||
|
// 这是一个抽象概念,实际实现需要修改Repository接口
|
||||||
|
|
||||||
|
// 当配置更新时,通知Repository清理缓存
|
||||||
|
go func() {
|
||||||
|
watcher := cm.AddConfigWatcher()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case key := <-watcher:
|
||||||
|
// 通知Repository层清理缓存(如果Repository支持)
|
||||||
|
utils.Debug("配置 %s 已更新,可能需要同步到Repository缓存", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRepositoryCache 当配置管理器更新配置时,通知Repository层同步
|
||||||
|
func (cm *ConfigManager) UpdateRepositoryCache(repoManager *repo.RepositoryManager) {
|
||||||
|
// 这个函数需要Repository支持特定的缓存清理方法
|
||||||
|
// 由于现有Repository没有提供这样的接口,我们只能依赖数据库同步
|
||||||
|
utils.Info("配置已通过配置管理器更新,Repository层将从数据库重新加载")
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ctwj/urldb/db/entity"
|
"github.com/ctwj/urldb/db/entity"
|
||||||
@@ -45,8 +47,22 @@ func InitDB() error {
|
|||||||
host, port, user, password, dbname)
|
host, port, user, password, dbname)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
// 配置慢查询日志
|
||||||
|
slowThreshold := getEnvInt("DB_SLOW_THRESHOLD_MS", 200)
|
||||||
|
logLevel := logger.Info
|
||||||
|
if os.Getenv("ENV") == "production" {
|
||||||
|
logLevel = logger.Warn
|
||||||
|
}
|
||||||
|
|
||||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Info),
|
Logger: logger.New(
|
||||||
|
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||||
|
logger.Config{
|
||||||
|
SlowThreshold: time.Duration(slowThreshold) * time.Millisecond,
|
||||||
|
LogLevel: logLevel,
|
||||||
|
Colorful: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -58,10 +74,17 @@ func InitDB() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置连接池参数
|
// 优化数据库连接池参数
|
||||||
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
|
maxOpenConns := getEnvInt("DB_MAX_OPEN_CONNS", 50)
|
||||||
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
|
maxIdleConns := getEnvInt("DB_MAX_IDLE_CONNS", 20)
|
||||||
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
|
connMaxLifetime := getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 30)
|
||||||
|
|
||||||
|
sqlDB.SetMaxOpenConns(maxOpenConns) // 最大打开连接数
|
||||||
|
sqlDB.SetMaxIdleConns(maxIdleConns) // 最大空闲连接数
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Minute) // 连接最大生命周期
|
||||||
|
|
||||||
|
utils.Info("数据库连接池配置 - 最大连接: %d, 空闲连接: %d, 生命周期: %d分钟",
|
||||||
|
maxOpenConns, maxIdleConns, connMaxLifetime)
|
||||||
|
|
||||||
// 检查是否需要迁移(只在开发环境或首次启动时)
|
// 检查是否需要迁移(只在开发环境或首次启动时)
|
||||||
if shouldRunMigration() {
|
if shouldRunMigration() {
|
||||||
@@ -300,3 +323,19 @@ func insertDefaultDataIfEmpty() error {
|
|||||||
utils.Info("默认数据插入完成")
|
utils.Info("默认数据插入完成")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getEnvInt 获取环境变量中的整数值,如果不存在则返回默认值
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
intValue, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
utils.Warn("环境变量 %s 的值 '%s' 不是有效的整数,使用默认值 %d", key, value, defaultValue)
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
|||||||
114
db/repo/pagination.go
Normal file
114
db/repo/pagination.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaginationResult 分页查询结果
|
||||||
|
type PaginationResult[T any] struct {
|
||||||
|
Data []T `json:"data"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginationOptions 分页查询选项
|
||||||
|
type PaginationOptions struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
OrderBy string `json:"order_by"`
|
||||||
|
OrderDir string `json:"order_dir"` // asc or desc
|
||||||
|
Preloads []string `json:"preloads"` // 需要预加载的关联
|
||||||
|
Filters map[string]interface{} `json:"filters"` // 过滤条件
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultPaginationOptions 默认分页选项
|
||||||
|
func DefaultPaginationOptions() *PaginationOptions {
|
||||||
|
return &PaginationOptions{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 20,
|
||||||
|
OrderBy: "id",
|
||||||
|
OrderDir: "desc",
|
||||||
|
Preloads: []string{},
|
||||||
|
Filters: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginatedQuery 通用分页查询函数
|
||||||
|
func PaginatedQuery[T any](db *gorm.DB, options *PaginationOptions) (*PaginationResult[T], error) {
|
||||||
|
// 验证分页参数
|
||||||
|
if options.Page < 1 {
|
||||||
|
options.Page = 1
|
||||||
|
}
|
||||||
|
if options.PageSize < 1 || options.PageSize > 1000 {
|
||||||
|
options.PageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用预加载
|
||||||
|
query := db.Model(new(T))
|
||||||
|
for _, preload := range options.Preloads {
|
||||||
|
query = query.Preload(preload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用过滤条件
|
||||||
|
for key, value := range options.Filters {
|
||||||
|
// 处理特殊过滤条件
|
||||||
|
switch key {
|
||||||
|
case "search":
|
||||||
|
// 搜索条件需要特殊处理
|
||||||
|
if searchStr, ok := value.(string); ok && searchStr != "" {
|
||||||
|
query = query.Where("title ILIKE ? OR description ILIKE ?", "%"+searchStr+"%", "%"+searchStr+"%")
|
||||||
|
}
|
||||||
|
case "category_id":
|
||||||
|
if categoryID, ok := value.(uint); ok {
|
||||||
|
query = query.Where("category_id = ?", categoryID)
|
||||||
|
}
|
||||||
|
case "pan_id":
|
||||||
|
if panID, ok := value.(uint); ok {
|
||||||
|
query = query.Where("pan_id = ?", panID)
|
||||||
|
}
|
||||||
|
case "is_valid":
|
||||||
|
if isValid, ok := value.(bool); ok {
|
||||||
|
query = query.Where("is_valid = ?", isValid)
|
||||||
|
}
|
||||||
|
case "is_public":
|
||||||
|
if isPublic, ok := value.(bool); ok {
|
||||||
|
query = query.Where("is_public = ?", isPublic)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// 通用过滤条件
|
||||||
|
query = query.Where(key+" = ?", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用排序
|
||||||
|
orderClause := options.OrderBy + " " + options.OrderDir
|
||||||
|
query = query.Order(orderClause)
|
||||||
|
|
||||||
|
// 计算偏移量
|
||||||
|
offset := (options.Page - 1) * options.PageSize
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
var data []T
|
||||||
|
if err := query.Offset(offset).Limit(options.PageSize).Find(&data).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
totalPages := int((total + int64(options.PageSize) - 1) / int64(options.PageSize))
|
||||||
|
|
||||||
|
return &PaginationResult[T]{
|
||||||
|
Data: data,
|
||||||
|
Total: total,
|
||||||
|
Page: options.Page,
|
||||||
|
PageSize: options.PageSize,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -68,38 +68,21 @@ func (r *ResourceRepositoryImpl) FindWithRelations() ([]entity.Resource, error)
|
|||||||
|
|
||||||
// FindWithRelationsPaginated 分页查找包含关联关系的资源
|
// FindWithRelationsPaginated 分页查找包含关联关系的资源
|
||||||
func (r *ResourceRepositoryImpl) FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error) {
|
func (r *ResourceRepositoryImpl) FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error) {
|
||||||
var resources []entity.Resource
|
// 使用新的分页查询功能
|
||||||
var total int64
|
options := &PaginationOptions{
|
||||||
|
Page: page,
|
||||||
offset := (page - 1) * limit
|
PageSize: limit,
|
||||||
|
OrderBy: "updated_at",
|
||||||
// 优化查询:只预加载必要的关联,并添加排序
|
OrderDir: "desc",
|
||||||
db := r.db.Model(&entity.Resource{}).
|
Preloads: []string{"Category", "Pan"},
|
||||||
Preload("Category").
|
|
||||||
Preload("Pan").
|
|
||||||
Order("updated_at DESC") // 按更新时间倒序,显示最新内容
|
|
||||||
|
|
||||||
// 获取总数(使用缓存键)
|
|
||||||
cacheKey := fmt.Sprintf("resources_total_%d_%d", page, limit)
|
|
||||||
if cached, exists := r.cache[cacheKey]; exists {
|
|
||||||
if totalCached, ok := cached.(int64); ok {
|
|
||||||
total = totalCached
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := db.Count(&total).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
// 缓存总数(5分钟)
|
|
||||||
r.cache[cacheKey] = total
|
|
||||||
go func() {
|
|
||||||
time.Sleep(5 * time.Minute)
|
|
||||||
delete(r.cache, cacheKey)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取分页数据
|
result, err := PaginatedQuery[entity.Resource](r.db, options)
|
||||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
if err != nil {
|
||||||
return resources, total, err
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Data, result.Total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindByCategoryID 根据分类ID查找
|
// FindByCategoryID 根据分类ID查找
|
||||||
|
|||||||
18
go.mod
18
go.mod
@@ -13,14 +13,22 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/meilisearch/meilisearch-go v0.33.1
|
github.com/meilisearch/meilisearch-go v0.33.1
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.41.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.30.0
|
gorm.io/gorm v1.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -52,10 +60,10 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
golang.org/x/arch v0.19.0 // indirect
|
golang.org/x/arch v0.19.0 // indirect
|
||||||
golang.org/x/net v0.42.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
golang.org/x/text v0.27.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
26
go.sum
26
go.sum
@@ -1,10 +1,14 @@
|
|||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
@@ -94,12 +98,22 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||||
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
@@ -126,14 +140,20 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA
|
|||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
||||||
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -143,11 +163,15 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -156,6 +180,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
|||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
52
main.go
52
main.go
@@ -5,12 +5,15 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ctwj/urldb/config"
|
||||||
"github.com/ctwj/urldb/db"
|
"github.com/ctwj/urldb/db"
|
||||||
"github.com/ctwj/urldb/db/entity"
|
"github.com/ctwj/urldb/db/entity"
|
||||||
"github.com/ctwj/urldb/db/repo"
|
"github.com/ctwj/urldb/db/repo"
|
||||||
"github.com/ctwj/urldb/handlers"
|
"github.com/ctwj/urldb/handlers"
|
||||||
"github.com/ctwj/urldb/middleware"
|
"github.com/ctwj/urldb/middleware"
|
||||||
|
"github.com/ctwj/urldb/monitor"
|
||||||
"github.com/ctwj/urldb/scheduler"
|
"github.com/ctwj/urldb/scheduler"
|
||||||
"github.com/ctwj/urldb/services"
|
"github.com/ctwj/urldb/services"
|
||||||
"github.com/ctwj/urldb/task"
|
"github.com/ctwj/urldb/task"
|
||||||
@@ -85,6 +88,17 @@ func main() {
|
|||||||
// 创建Repository管理器
|
// 创建Repository管理器
|
||||||
repoManager := repo.NewRepositoryManager(db.DB)
|
repoManager := repo.NewRepositoryManager(db.DB)
|
||||||
|
|
||||||
|
// 创建配置管理器
|
||||||
|
configManager := config.NewConfigManager(repoManager)
|
||||||
|
|
||||||
|
// 设置全局配置管理器
|
||||||
|
config.SetGlobalConfigManager(configManager)
|
||||||
|
|
||||||
|
// 加载所有配置到缓存
|
||||||
|
if err := configManager.LoadAllConfigs(); err != nil {
|
||||||
|
utils.Error("加载配置缓存失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 创建任务管理器
|
// 创建任务管理器
|
||||||
taskManager := task.NewTaskManager(repoManager)
|
taskManager := task.NewTaskManager(repoManager)
|
||||||
|
|
||||||
@@ -112,7 +126,22 @@ func main() {
|
|||||||
utils.Info("任务管理器初始化完成")
|
utils.Info("任务管理器初始化完成")
|
||||||
|
|
||||||
// 创建Gin实例
|
// 创建Gin实例
|
||||||
r := gin.Default()
|
r := gin.New()
|
||||||
|
|
||||||
|
// 创建监控和错误处理器
|
||||||
|
metrics := monitor.GetGlobalMetrics()
|
||||||
|
errorHandler := monitor.GetGlobalErrorHandler()
|
||||||
|
if errorHandler == nil {
|
||||||
|
errorHandler = monitor.NewErrorHandler(1000, 24*time.Hour)
|
||||||
|
monitor.SetGlobalErrorHandler(errorHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加中间件
|
||||||
|
r.Use(gin.Logger()) // Gin日志中间件
|
||||||
|
r.Use(errorHandler.RecoverMiddleware()) // Panic恢复中间件
|
||||||
|
r.Use(errorHandler.ErrorMiddleware()) // 错误处理中间件
|
||||||
|
r.Use(metrics.MetricsMiddleware()) // 监控中间件
|
||||||
|
r.Use(gin.Recovery()) // Gin恢复中间件
|
||||||
|
|
||||||
// 配置CORS
|
// 配置CORS
|
||||||
config := cors.DefaultConfig()
|
config := cors.DefaultConfig()
|
||||||
@@ -366,6 +395,27 @@ func main() {
|
|||||||
api.POST("/telegram/webhook", telegramHandler.HandleWebhook)
|
api.POST("/telegram/webhook", telegramHandler.HandleWebhook)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置监控系统
|
||||||
|
monitor.SetupMonitoring(r)
|
||||||
|
|
||||||
|
// 添加测试路由(仅在开发环境)
|
||||||
|
if os.Getenv("ENV") != "production" {
|
||||||
|
r.GET("/test/metrics", monitor.TestMetrics)
|
||||||
|
r.GET("/test/error", monitor.TestError)
|
||||||
|
r.GET("/test/error-stats", monitor.GetErrorStats)
|
||||||
|
r.GET("/test/metrics-stats", monitor.GetMetricsStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动监控服务器
|
||||||
|
metricsConfig := &monitor.MetricsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
ListenAddress: ":9090",
|
||||||
|
MetricsPath: "/metrics",
|
||||||
|
Namespace: "urldb",
|
||||||
|
Subsystem: "api",
|
||||||
|
}
|
||||||
|
metrics.StartMetricsServer(metricsConfig)
|
||||||
|
|
||||||
// 静态文件服务
|
// 静态文件服务
|
||||||
r.Static("/uploads", "./uploads")
|
r.Static("/uploads", "./uploads")
|
||||||
|
|
||||||
|
|||||||
327
monitor/error_handler.go
Normal file
327
monitor/error_handler.go
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
package monitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ctwj/urldb/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorInfo 错误信息结构
|
||||||
|
type ErrorInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
StackTrace string `json:"stack_trace"`
|
||||||
|
RequestInfo *RequestInfo `json:"request_info,omitempty"`
|
||||||
|
Level string `json:"level"` // error, warn, info
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestInfo 请求信息结构
|
||||||
|
type RequestInfo struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
RequestBody string `json:"request_body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorHandler 错误处理器
|
||||||
|
type ErrorHandler struct {
|
||||||
|
errors map[string]*ErrorInfo
|
||||||
|
mu sync.RWMutex
|
||||||
|
maxErrors int
|
||||||
|
retention time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorHandler 创建新的错误处理器
|
||||||
|
func NewErrorHandler(maxErrors int, retention time.Duration) *ErrorHandler {
|
||||||
|
eh := &ErrorHandler{
|
||||||
|
errors: make(map[string]*ErrorInfo),
|
||||||
|
maxErrors: maxErrors,
|
||||||
|
retention: retention,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动错误清理协程
|
||||||
|
go eh.cleanupRoutine()
|
||||||
|
|
||||||
|
return eh
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverMiddleware panic恢复中间件
|
||||||
|
func (eh *ErrorHandler) RecoverMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
// 记录错误信息
|
||||||
|
stackTrace := getStackTrace()
|
||||||
|
|
||||||
|
errorInfo := &ErrorInfo{
|
||||||
|
ID: fmt.Sprintf("panic_%d", time.Now().UnixNano()),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Message: fmt.Sprintf("%v", err),
|
||||||
|
StackTrace: stackTrace,
|
||||||
|
RequestInfo: &RequestInfo{
|
||||||
|
Method: c.Request.Method,
|
||||||
|
URL: c.Request.URL.String(),
|
||||||
|
RemoteAddr: c.ClientIP(),
|
||||||
|
UserAgent: c.GetHeader("User-Agent"),
|
||||||
|
},
|
||||||
|
Level: "error",
|
||||||
|
Count: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存错误信息
|
||||||
|
eh.saveError(errorInfo)
|
||||||
|
|
||||||
|
utils.Error("Panic recovered: %v\nStack trace: %s", err, stackTrace)
|
||||||
|
|
||||||
|
// 返回错误响应
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Internal server error",
|
||||||
|
"code": "INTERNAL_ERROR",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 不继续处理
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorMiddleware 通用错误处理中间件
|
||||||
|
func (eh *ErrorHandler) ErrorMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 检查是否有错误
|
||||||
|
if len(c.Errors) > 0 {
|
||||||
|
for _, ginErr := range c.Errors {
|
||||||
|
errorInfo := &ErrorInfo{
|
||||||
|
ID: fmt.Sprintf("error_%d_%s", time.Now().UnixNano(), ginErr.Type),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Message: ginErr.Error(),
|
||||||
|
Level: "error",
|
||||||
|
Count: 1,
|
||||||
|
RequestInfo: &RequestInfo{
|
||||||
|
Method: c.Request.Method,
|
||||||
|
URL: c.Request.URL.String(),
|
||||||
|
RemoteAddr: c.ClientIP(),
|
||||||
|
UserAgent: c.GetHeader("User-Agent"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
eh.saveError(errorInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveError 保存错误信息
|
||||||
|
func (eh *ErrorHandler) saveError(errorInfo *ErrorInfo) {
|
||||||
|
eh.mu.Lock()
|
||||||
|
defer eh.mu.Unlock()
|
||||||
|
|
||||||
|
key := errorInfo.Message
|
||||||
|
if existing, exists := eh.errors[key]; exists {
|
||||||
|
// 如果错误已存在,增加计数
|
||||||
|
existing.Count++
|
||||||
|
existing.Timestamp = time.Now()
|
||||||
|
} else {
|
||||||
|
// 如果是新错误,添加到映射中
|
||||||
|
eh.errors[key] = errorInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果错误数量超过限制,清理旧错误
|
||||||
|
if len(eh.errors) > eh.maxErrors {
|
||||||
|
eh.cleanupOldErrors()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrors 获取错误列表
|
||||||
|
func (eh *ErrorHandler) GetErrors() []*ErrorInfo {
|
||||||
|
eh.mu.RLock()
|
||||||
|
defer eh.mu.RUnlock()
|
||||||
|
|
||||||
|
errors := make([]*ErrorInfo, 0, len(eh.errors))
|
||||||
|
for _, errorInfo := range eh.errors {
|
||||||
|
errors = append(errors, errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrorByID 根据ID获取错误
|
||||||
|
func (eh *ErrorHandler) GetErrorByID(id string) (*ErrorInfo, bool) {
|
||||||
|
eh.mu.RLock()
|
||||||
|
defer eh.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, errorInfo := range eh.errors {
|
||||||
|
if errorInfo.ID == id {
|
||||||
|
return errorInfo, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearErrors 清空所有错误
|
||||||
|
func (eh *ErrorHandler) ClearErrors() {
|
||||||
|
eh.mu.Lock()
|
||||||
|
defer eh.mu.Unlock()
|
||||||
|
|
||||||
|
eh.errors = make(map[string]*ErrorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupOldErrors 清理旧错误
|
||||||
|
func (eh *ErrorHandler) cleanupOldErrors() {
|
||||||
|
// 简单策略:保留最近的错误,删除旧的
|
||||||
|
errors := make([]*ErrorInfo, 0, len(eh.errors))
|
||||||
|
for _, errorInfo := range eh.errors {
|
||||||
|
errors = append(errors, errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间戳排序
|
||||||
|
for i := 0; i < len(errors)-1; i++ {
|
||||||
|
for j := i + 1; j < len(errors); j++ {
|
||||||
|
if errors[i].Timestamp.Before(errors[j].Timestamp) {
|
||||||
|
errors[i], errors[j] = errors[j], errors[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留最新的maxErrors/2个错误
|
||||||
|
keep := eh.maxErrors / 2
|
||||||
|
if keep < 1 {
|
||||||
|
keep = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > keep {
|
||||||
|
// 重建错误映射
|
||||||
|
newErrors := make(map[string]*ErrorInfo)
|
||||||
|
for i := 0; i < keep; i++ {
|
||||||
|
newErrors[errors[i].Message] = errors[i]
|
||||||
|
}
|
||||||
|
eh.errors = newErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupRoutine 定期清理过期错误的协程
|
||||||
|
func (eh *ErrorHandler) cleanupRoutine() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute) // 每5分钟清理一次
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
eh.mu.Lock()
|
||||||
|
for key, errorInfo := range eh.errors {
|
||||||
|
if time.Since(errorInfo.Timestamp) > eh.retention {
|
||||||
|
delete(eh.errors, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eh.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStackTrace 获取堆栈跟踪信息
|
||||||
|
func getStackTrace() string {
|
||||||
|
var buf [4096]byte
|
||||||
|
n := runtime.Stack(buf[:], false)
|
||||||
|
return string(buf[:n])
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrorStatistics 获取错误统计信息
|
||||||
|
func (eh *ErrorHandler) GetErrorStatistics() map[string]interface{} {
|
||||||
|
eh.mu.RLock()
|
||||||
|
defer eh.mu.RUnlock()
|
||||||
|
|
||||||
|
totalErrors := len(eh.errors)
|
||||||
|
totalCount := 0
|
||||||
|
errorTypes := make(map[string]int)
|
||||||
|
|
||||||
|
for _, errorInfo := range eh.errors {
|
||||||
|
totalCount += errorInfo.Count
|
||||||
|
// 提取错误类型(基于错误消息的前几个单词)
|
||||||
|
parts := strings.Split(errorInfo.Message, " ")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
errorType := strings.Join(parts[:min(3, len(parts))], " ")
|
||||||
|
errorTypes[errorType]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"total_errors": totalErrors,
|
||||||
|
"total_count": totalCount,
|
||||||
|
"error_types": errorTypes,
|
||||||
|
"max_errors": eh.maxErrors,
|
||||||
|
"retention": eh.retention,
|
||||||
|
"active_errors": len(eh.errors),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// min 辅助函数
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalErrorHandler 全局错误处理器
|
||||||
|
var globalErrorHandler *ErrorHandler
|
||||||
|
|
||||||
|
// InitGlobalErrorHandler 初始化全局错误处理器
|
||||||
|
func InitGlobalErrorHandler(maxErrors int, retention time.Duration) {
|
||||||
|
globalErrorHandler = NewErrorHandler(maxErrors, retention)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalErrorHandler 获取全局错误处理器
|
||||||
|
func GetGlobalErrorHandler() *ErrorHandler {
|
||||||
|
if globalErrorHandler == nil {
|
||||||
|
InitGlobalErrorHandler(100, 24*time.Hour)
|
||||||
|
}
|
||||||
|
return globalErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover 全局panic恢复函数
|
||||||
|
func Recover() gin.HandlerFunc {
|
||||||
|
if globalErrorHandler == nil {
|
||||||
|
InitGlobalErrorHandler(100, 24*time.Hour)
|
||||||
|
}
|
||||||
|
return globalErrorHandler.RecoverMiddleware()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 全局错误处理函数
|
||||||
|
func Error() gin.HandlerFunc {
|
||||||
|
if globalErrorHandler == nil {
|
||||||
|
InitGlobalErrorHandler(100, 24*time.Hour)
|
||||||
|
}
|
||||||
|
return globalErrorHandler.ErrorMiddleware()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordError 记录错误(全局函数)
|
||||||
|
func RecordError(message string, level string) {
|
||||||
|
if globalErrorHandler == nil {
|
||||||
|
InitGlobalErrorHandler(100, 24*time.Hour)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorInfo := &ErrorInfo{
|
||||||
|
ID: fmt.Sprintf("%s_%d", level, time.Now().UnixNano()),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Message: message,
|
||||||
|
Level: level,
|
||||||
|
Count: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
globalErrorHandler.saveError(errorInfo)
|
||||||
|
}
|
||||||
458
monitor/metrics.go
Normal file
458
monitor/metrics.go
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
package monitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ctwj/urldb/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Metrics 监控指标
|
||||||
|
type Metrics struct {
|
||||||
|
// HTTP请求指标
|
||||||
|
RequestsTotal *prometheus.CounterVec
|
||||||
|
RequestDuration *prometheus.HistogramVec
|
||||||
|
RequestSize *prometheus.SummaryVec
|
||||||
|
ResponseSize *prometheus.SummaryVec
|
||||||
|
|
||||||
|
// 数据库指标
|
||||||
|
DatabaseQueries *prometheus.CounterVec
|
||||||
|
DatabaseErrors *prometheus.CounterVec
|
||||||
|
DatabaseDuration *prometheus.HistogramVec
|
||||||
|
|
||||||
|
// 系统指标
|
||||||
|
MemoryUsage prometheus.Gauge
|
||||||
|
Goroutines prometheus.Gauge
|
||||||
|
GCStats *prometheus.CounterVec
|
||||||
|
|
||||||
|
// 业务指标
|
||||||
|
ResourcesCreated *prometheus.CounterVec
|
||||||
|
ResourcesViewed *prometheus.CounterVec
|
||||||
|
Searches *prometheus.CounterVec
|
||||||
|
Transfers *prometheus.CounterVec
|
||||||
|
|
||||||
|
// 错误指标
|
||||||
|
ErrorsTotal *prometheus.CounterVec
|
||||||
|
|
||||||
|
// 自定义指标
|
||||||
|
CustomCounters map[string]prometheus.Counter
|
||||||
|
CustomGauges map[string]prometheus.Gauge
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetricsConfig 监控配置
|
||||||
|
type MetricsConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
ListenAddress string
|
||||||
|
MetricsPath string
|
||||||
|
Namespace string
|
||||||
|
Subsystem string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultMetricsConfig 默认监控配置
|
||||||
|
func DefaultMetricsConfig() *MetricsConfig {
|
||||||
|
return &MetricsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
ListenAddress: ":9090",
|
||||||
|
MetricsPath: "/metrics",
|
||||||
|
Namespace: "urldb",
|
||||||
|
Subsystem: "api",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalMetrics 全局监控实例
|
||||||
|
var (
|
||||||
|
globalMetrics *Metrics
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewMetrics 创建新的监控指标
|
||||||
|
func NewMetrics(config *MetricsConfig) *Metrics {
|
||||||
|
if config == nil {
|
||||||
|
config = DefaultMetricsConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := config.Namespace
|
||||||
|
subsystem := config.Subsystem
|
||||||
|
|
||||||
|
m := &Metrics{
|
||||||
|
// HTTP请求指标
|
||||||
|
RequestsTotal: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "http_requests_total",
|
||||||
|
Help: "Total number of HTTP requests",
|
||||||
|
},
|
||||||
|
[]string{"method", "endpoint", "status"},
|
||||||
|
),
|
||||||
|
RequestDuration: promauto.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "http_request_duration_seconds",
|
||||||
|
Help: "HTTP request duration in seconds",
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
[]string{"method", "endpoint", "status"},
|
||||||
|
),
|
||||||
|
RequestSize: promauto.NewSummaryVec(
|
||||||
|
prometheus.SummaryOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "http_request_size_bytes",
|
||||||
|
Help: "HTTP request size in bytes",
|
||||||
|
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
||||||
|
},
|
||||||
|
[]string{"method", "endpoint"},
|
||||||
|
),
|
||||||
|
ResponseSize: promauto.NewSummaryVec(
|
||||||
|
prometheus.SummaryOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "http_response_size_bytes",
|
||||||
|
Help: "HTTP response size in bytes",
|
||||||
|
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
||||||
|
},
|
||||||
|
[]string{"method", "endpoint"},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 数据库指标
|
||||||
|
DatabaseQueries: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "database",
|
||||||
|
Name: "queries_total",
|
||||||
|
Help: "Total number of database queries",
|
||||||
|
},
|
||||||
|
[]string{"table", "operation"},
|
||||||
|
),
|
||||||
|
DatabaseErrors: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "database",
|
||||||
|
Name: "errors_total",
|
||||||
|
Help: "Total number of database errors",
|
||||||
|
},
|
||||||
|
[]string{"table", "operation", "error"},
|
||||||
|
),
|
||||||
|
DatabaseDuration: promauto.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "database",
|
||||||
|
Name: "query_duration_seconds",
|
||||||
|
Help: "Database query duration in seconds",
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
[]string{"table", "operation"},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 系统指标
|
||||||
|
MemoryUsage: promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "system",
|
||||||
|
Name: "memory_usage_bytes",
|
||||||
|
Help: "Current memory usage in bytes",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Goroutines: promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "system",
|
||||||
|
Name: "goroutines",
|
||||||
|
Help: "Number of goroutines",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GCStats: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "system",
|
||||||
|
Name: "gc_stats_total",
|
||||||
|
Help: "Garbage collection statistics",
|
||||||
|
},
|
||||||
|
[]string{"type"},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 业务指标
|
||||||
|
ResourcesCreated: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "business",
|
||||||
|
Name: "resources_created_total",
|
||||||
|
Help: "Total number of resources created",
|
||||||
|
},
|
||||||
|
[]string{"category", "platform"},
|
||||||
|
),
|
||||||
|
ResourcesViewed: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "business",
|
||||||
|
Name: "resources_viewed_total",
|
||||||
|
Help: "Total number of resources viewed",
|
||||||
|
},
|
||||||
|
[]string{"category"},
|
||||||
|
),
|
||||||
|
Searches: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "business",
|
||||||
|
Name: "searches_total",
|
||||||
|
Help: "Total number of searches",
|
||||||
|
},
|
||||||
|
[]string{"platform"},
|
||||||
|
),
|
||||||
|
Transfers: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "business",
|
||||||
|
Name: "transfers_total",
|
||||||
|
Help: "Total number of transfers",
|
||||||
|
},
|
||||||
|
[]string{"platform", "status"},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 错误指标
|
||||||
|
ErrorsTotal: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "errors",
|
||||||
|
Name: "total",
|
||||||
|
Help: "Total number of errors",
|
||||||
|
},
|
||||||
|
[]string{"type", "endpoint"},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 自定义指标
|
||||||
|
CustomCounters: make(map[string]prometheus.Counter),
|
||||||
|
CustomGauges: make(map[string]prometheus.Gauge),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动系统指标收集
|
||||||
|
go m.collectSystemMetrics()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalMetrics 获取全局监控实例
|
||||||
|
func GetGlobalMetrics() *Metrics {
|
||||||
|
once.Do(func() {
|
||||||
|
globalMetrics = NewMetrics(DefaultMetricsConfig())
|
||||||
|
})
|
||||||
|
return globalMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalMetrics 设置全局监控实例
|
||||||
|
func SetGlobalMetrics(metrics *Metrics) {
|
||||||
|
globalMetrics = metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectSystemMetrics 收集系统指标
|
||||||
|
func (m *Metrics) collectSystemMetrics() {
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
// 收集内存使用情况
|
||||||
|
var ms runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&ms)
|
||||||
|
m.MemoryUsage.Set(float64(ms.Alloc))
|
||||||
|
|
||||||
|
// 收集goroutine数量
|
||||||
|
m.Goroutines.Set(float64(runtime.NumGoroutine()))
|
||||||
|
|
||||||
|
// 收集GC统计
|
||||||
|
m.GCStats.WithLabelValues("alloc").Add(float64(ms.TotalAlloc))
|
||||||
|
m.GCStats.WithLabelValues("sys").Add(float64(ms.Sys))
|
||||||
|
m.GCStats.WithLabelValues("lookups").Add(float64(ms.Lookups))
|
||||||
|
m.GCStats.WithLabelValues("mallocs").Add(float64(ms.Mallocs))
|
||||||
|
m.GCStats.WithLabelValues("frees").Add(float64(ms.Frees))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetricsMiddleware 监控中间件
|
||||||
|
func (m *Metrics) MetricsMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
path := c.FullPath()
|
||||||
|
|
||||||
|
// 如果没有匹配的路由,使用请求路径
|
||||||
|
if path == "" {
|
||||||
|
path = c.Request.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录请求大小
|
||||||
|
requestSize := float64(c.Request.ContentLength)
|
||||||
|
m.RequestSize.WithLabelValues(c.Request.Method, path).Observe(requestSize)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 记录响应信息
|
||||||
|
status := c.Writer.Status()
|
||||||
|
latency := time.Since(start).Seconds()
|
||||||
|
responseSize := float64(c.Writer.Size())
|
||||||
|
|
||||||
|
// 更新指标
|
||||||
|
m.RequestsTotal.WithLabelValues(c.Request.Method, path, fmt.Sprintf("%d", status)).Inc()
|
||||||
|
m.RequestDuration.WithLabelValues(c.Request.Method, path, fmt.Sprintf("%d", status)).Observe(latency)
|
||||||
|
m.ResponseSize.WithLabelValues(c.Request.Method, path).Observe(responseSize)
|
||||||
|
|
||||||
|
// 如果是错误状态码,记录错误
|
||||||
|
if status >= 400 {
|
||||||
|
m.ErrorsTotal.WithLabelValues("http", path).Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartMetricsServer 启动监控服务器
|
||||||
|
func (m *Metrics) StartMetricsServer(config *MetricsConfig) {
|
||||||
|
if config == nil {
|
||||||
|
config = DefaultMetricsConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.Enabled {
|
||||||
|
utils.Info("监控服务器未启用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的Gin路由器
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
|
||||||
|
// 注册Prometheus指标端点
|
||||||
|
router.GET(config.MetricsPath, gin.WrapH(promhttp.Handler()))
|
||||||
|
|
||||||
|
// 启动HTTP服务器
|
||||||
|
go func() {
|
||||||
|
utils.Info("监控服务器启动在 %s", config.ListenAddress)
|
||||||
|
if err := router.Run(config.ListenAddress); err != nil {
|
||||||
|
utils.Error("监控服务器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
utils.Info("监控服务器已启动,指标路径: %s%s", config.ListenAddress, config.MetricsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementDatabaseQuery 增加数据库查询计数
|
||||||
|
func (m *Metrics) IncrementDatabaseQuery(table, operation string) {
|
||||||
|
m.DatabaseQueries.WithLabelValues(table, operation).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementDatabaseError 增加数据库错误计数
|
||||||
|
func (m *Metrics) IncrementDatabaseError(table, operation, error string) {
|
||||||
|
m.DatabaseErrors.WithLabelValues(table, operation, error).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObserveDatabaseDuration 记录数据库查询耗时
|
||||||
|
func (m *Metrics) ObserveDatabaseDuration(table, operation string, duration float64) {
|
||||||
|
m.DatabaseDuration.WithLabelValues(table, operation).Observe(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementResourceCreated 增加资源创建计数
|
||||||
|
func (m *Metrics) IncrementResourceCreated(category, platform string) {
|
||||||
|
m.ResourcesCreated.WithLabelValues(category, platform).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementResourceViewed 增加资源查看计数
|
||||||
|
func (m *Metrics) IncrementResourceViewed(category string) {
|
||||||
|
m.ResourcesViewed.WithLabelValues(category).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementSearch 增加搜索计数
|
||||||
|
func (m *Metrics) IncrementSearch(platform string) {
|
||||||
|
m.Searches.WithLabelValues(platform).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementTransfer 增加转存计数
|
||||||
|
func (m *Metrics) IncrementTransfer(platform, status string) {
|
||||||
|
m.Transfers.WithLabelValues(platform, status).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementError 增加错误计数
|
||||||
|
func (m *Metrics) IncrementError(errorType, endpoint string) {
|
||||||
|
m.ErrorsTotal.WithLabelValues(errorType, endpoint).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomCounter 添加自定义计数器
|
||||||
|
func (m *Metrics) AddCustomCounter(name, help string, labels []string) prometheus.Counter {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%s_%v", name, labels)
|
||||||
|
if counter, exists := m.CustomCounters[key]; exists {
|
||||||
|
return counter
|
||||||
|
}
|
||||||
|
|
||||||
|
counter := promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: "urldb",
|
||||||
|
Name: name,
|
||||||
|
Help: help,
|
||||||
|
},
|
||||||
|
labels,
|
||||||
|
).WithLabelValues() // 如果没有标签,返回默认实例
|
||||||
|
|
||||||
|
m.CustomCounters[key] = counter
|
||||||
|
return counter
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomGauge 添加自定义仪表盘
|
||||||
|
func (m *Metrics) AddCustomGauge(name, help string, labels []string) prometheus.Gauge {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%s_%v", name, labels)
|
||||||
|
if gauge, exists := m.CustomGauges[key]; exists {
|
||||||
|
return gauge
|
||||||
|
}
|
||||||
|
|
||||||
|
gauge := promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: "urldb",
|
||||||
|
Name: name,
|
||||||
|
Help: help,
|
||||||
|
},
|
||||||
|
labels,
|
||||||
|
).WithLabelValues() // 如果没有标签,返回默认实例
|
||||||
|
|
||||||
|
m.CustomGauges[key] = gauge
|
||||||
|
return gauge
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetricsSummary 获取指标摘要
|
||||||
|
func (m *Metrics) GetMetricsSummary() map[string]interface{} {
|
||||||
|
// 这里可以实现获取当前指标摘要的逻辑
|
||||||
|
// 由于Prometheus指标不能直接读取,我们只能返回一些基本的统计信息
|
||||||
|
return map[string]interface{}{
|
||||||
|
"timestamp": time.Now(),
|
||||||
|
"status": "running",
|
||||||
|
"info": "使用 /metrics 端点获取详细指标",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck 健康检查
|
||||||
|
func (m *Metrics) HealthCheck(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
"version": "1.0.0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupHealthCheck 设置健康检查端点
|
||||||
|
func (m *Metrics) SetupHealthCheck(router *gin.Engine) {
|
||||||
|
router.GET("/health", m.HealthCheck)
|
||||||
|
router.GET("/healthz", m.HealthCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetricsHandler 指标处理器
|
||||||
|
func (m *Metrics) MetricsHandler() gin.HandlerFunc {
|
||||||
|
return gin.WrapH(promhttp.Handler())
|
||||||
|
}
|
||||||
25
monitor/setup.go
Normal file
25
monitor/setup.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package monitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ctwj/urldb/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupMonitoring 设置完整的监控系统
|
||||||
|
func SetupMonitoring(router *gin.Engine) {
|
||||||
|
// 获取全局监控实例
|
||||||
|
metrics := GetGlobalMetrics()
|
||||||
|
|
||||||
|
// 设置健康检查端点
|
||||||
|
metrics.SetupHealthCheck(router)
|
||||||
|
|
||||||
|
// 设置指标端点
|
||||||
|
router.GET("/metrics", metrics.MetricsHandler())
|
||||||
|
|
||||||
|
utils.Info("监控系统已设置完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalErrorHandler 设置全局错误处理器
|
||||||
|
func SetGlobalErrorHandler(eh *ErrorHandler) {
|
||||||
|
globalErrorHandler = eh
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user