random-api-go/services/csv_service.go
wood chen 80d18f2b88 feat(config, handlers, middleware): enhance configuration and API request handling
- Added performance settings to the configuration, including max concurrent requests and caching options.
- Updated API request handling to use context for timeouts and improved logging with Unix timestamps.
- Introduced rate limiting middleware to manage request load effectively.
- Enhanced metrics logging to include atomic counters for request counts and improved data structure for performance metrics.
- Implemented caching for CSV content to optimize data retrieval and reduce load times.
2024-12-01 00:14:21 +08:00

243 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package services
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"random-api-go/config"
"random-api-go/models"
"random-api-go/utils"
"strings"
"sync"
"time"
)
type CSVCache struct {
selector *models.URLSelector
lastCheck time.Time
mu sync.RWMutex
}
var (
CSVPathsCache map[string]map[string]string
csvCache = make(map[string]*CSVCache)
cacheTTL = 1 * time.Hour
Mu sync.RWMutex
)
// InitializeCSVService 初始化CSV服务
func InitializeCSVService() error {
// 加载url.json
if err := LoadCSVPaths(); err != nil {
return fmt.Errorf("failed to load CSV paths: %v", err)
}
// 获取一个CSVPathsCache的副本避免长时间持有锁
Mu.RLock()
pathsCopy := make(map[string]map[string]string)
for prefix, suffixMap := range CSVPathsCache {
pathsCopy[prefix] = make(map[string]string)
for suffix, path := range suffixMap {
pathsCopy[prefix][suffix] = path
}
}
Mu.RUnlock()
// 使用副本进行初始化
for prefix, suffixMap := range pathsCopy {
for suffix, csvPath := range suffixMap {
selector, err := GetCSVContent(csvPath)
if err != nil {
log.Printf("Warning: Failed to load CSV content for %s/%s: %v", prefix, suffix, err)
continue
}
// 更新URL计数
endpoint := fmt.Sprintf("%s/%s", prefix, suffix)
UpdateURLCount(endpoint, csvPath, len(selector.URLs))
log.Printf("Loaded %d URLs for endpoint: %s/%s", len(selector.URLs), prefix, suffix)
}
}
return nil
}
func LoadCSVPaths() error {
var data []byte
var err error
// 获取环境变量中的基础URL
baseURL := os.Getenv(config.EnvBaseURL)
if baseURL != "" {
// 构建完整的URL
var fullURL string
if strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://") {
fullURL = utils.JoinURLPath(baseURL, "url.json")
} else {
fullURL = "https://" + utils.JoinURLPath(baseURL, "url.json")
}
log.Printf("Attempting to read url.json from: %s", fullURL)
// 创建HTTP客户端
client := &http.Client{
Timeout: config.RequestTimeout,
}
resp, err := client.Get(fullURL)
if err != nil {
return fmt.Errorf("failed to fetch url.json: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch url.json, status code: %d", resp.StatusCode)
}
data, err = io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read url.json response: %w", err)
}
} else {
// 从本地文件读取
jsonPath := filepath.Join("public", "url.json")
log.Printf("Attempting to read local file: %s", jsonPath)
data, err = os.ReadFile(jsonPath)
if err != nil {
return fmt.Errorf("failed to read local url.json: %w", err)
}
}
var result map[string]map[string]string
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("failed to unmarshal url.json: %w", err)
}
Mu.Lock()
CSVPathsCache = result
Mu.Unlock()
log.Println("CSV paths loaded from url.json")
return nil
}
func GetCSVContent(path string) (*models.URLSelector, error) {
cache, ok := csvCache[path]
if ok {
cache.mu.RLock()
if time.Since(cache.lastCheck) < cacheTTL {
defer cache.mu.RUnlock()
return cache.selector, nil
}
cache.mu.RUnlock()
}
// 更新缓存
selector, err := loadCSVContent(path)
if err != nil {
return nil, err
}
cache = &CSVCache{
selector: selector,
lastCheck: time.Now(),
}
csvCache[path] = cache
return selector, nil
}
func loadCSVContent(path string) (*models.URLSelector, error) {
// log.Printf("开始获取CSV内容: %s", path)
Mu.RLock()
selector, exists := csvCache[path]
Mu.RUnlock()
if exists {
// log.Printf("从缓存中获取到CSV内容: %s", path)
return selector.selector, nil
}
var fileContent []byte
var err error
baseURL := os.Getenv(config.EnvBaseURL)
if baseURL != "" {
var fullURL string
if strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://") {
fullURL = utils.JoinURLPath(baseURL, path)
} else {
fullURL = "https://" + utils.JoinURLPath(baseURL, path)
}
log.Printf("尝试从URL获取: %s", fullURL)
client := &http.Client{
Timeout: config.RequestTimeout,
}
resp, err := client.Get(fullURL)
if err != nil {
log.Printf("HTTP请求失败: %v", err)
return nil, fmt.Errorf("HTTP请求失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("HTTP请求返回非200状态码: %d", resp.StatusCode)
return nil, fmt.Errorf("HTTP请求返回非200状态码: %d", resp.StatusCode)
}
fileContent, err = io.ReadAll(resp.Body)
if err != nil {
log.Printf("读取响应内容失败: %v", err)
return nil, fmt.Errorf("读取响应内容失败: %w", err)
}
log.Printf("成功读取到CSV内容长度: %d bytes", len(fileContent))
} else {
// 如果没有设置基础URL从本地文件读取
fullPath := filepath.Join("public", path)
log.Printf("尝试读取本地文件: %s", fullPath)
fileContent, err = os.ReadFile(fullPath)
if err != nil {
return nil, fmt.Errorf("读取CSV内容时出错: %w", err)
}
}
lines := strings.Split(string(fileContent), "\n")
log.Printf("CSV文件包含 %d 行", len(lines))
uniqueURLs := make(map[string]bool)
var fileArray []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && !uniqueURLs[trimmed] {
fileArray = append(fileArray, trimmed)
uniqueURLs[trimmed] = true
}
}
log.Printf("处理后得到 %d 个唯一URL", len(fileArray))
urlSelector := models.NewURLSelector(fileArray)
Mu.Lock()
csvCache[path] = &CSVCache{
selector: urlSelector,
lastCheck: time.Now(),
}
Mu.Unlock()
return urlSelector, nil
}