Skip to content

Commit 26cf1bd

Browse files
committed
feat(config): 添加从D1拉取配置功能并优化同步机制
新增从D1数据库拉取配置的功能,包括后端API接口、前端UI按钮和配置同步逻辑。同时优化了D1同步机制,移除了定时同步循环,改为按需同步。添加了路径统计数据的同步功能。
1 parent 90d70b2 commit 26cf1bd

File tree

7 files changed

+171
-114
lines changed

7 files changed

+171
-114
lines changed

cloudflare-worker/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"deploy": "wrangler deploy",
99
"tail": "wrangler tail",
1010
"d1:create": "wrangler d1 create proxy-go-data",
11-
"d1:migrations": "wrangler d1 migrations apply proxy-go-data"
11+
"d1:migrations": "wrangler d1 migrations apply proxy-go-data",
12+
"d1:remote": "wrangler d1 migrations apply proxy-go-data --remote"
1213
},
1314
"keywords": [],
1415
"author": "",

internal/handler/config.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ func (h *ConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
2626
h.handleGetConfig(w, r)
2727
case "/admin/api/config/save":
2828
h.handleSaveConfig(w, r)
29+
case "/admin/api/config/pull":
30+
h.handlePullConfig(w, r)
2931
default:
3032
http.NotFound(w, r)
3133
}
@@ -68,3 +70,21 @@ func (h *ConfigHandler) handleSaveConfig(w http.ResponseWriter, r *http.Request)
6870
w.Write([]byte(`{"message": "配置已更新并生效"}`))
6971
}
7072

73+
// handlePullConfig 处理从 D1 拉取配置请求
74+
func (h *ConfigHandler) handlePullConfig(w http.ResponseWriter, r *http.Request) {
75+
if r.Method != http.MethodPost {
76+
http.Error(w, "方法不允许", http.StatusMethodNotAllowed)
77+
return
78+
}
79+
80+
// 从 D1 拉取配置
81+
configData, err := h.configService.PullConfigFromD1()
82+
if err != nil {
83+
http.Error(w, "从 D1 拉取配置失败: "+err.Error(), http.StatusInternalServerError)
84+
return
85+
}
86+
87+
w.Header().Set("Content-Type", "application/json")
88+
w.Write(configData)
89+
}
90+

internal/metrics/persistence.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ func (ms *MetricsStorage) SaveMetrics() error {
109109
}
110110
}
111111

112+
// 获取路径统计
113+
pathStats := ms.getPathStatsArray()
114+
if len(pathStats) > 0 {
115+
if err := sync.SavePathStats(ctx, pathStats); err != nil {
116+
log.Printf("[MetricsStorage] 保存路径统计失败: %v", err)
117+
}
118+
}
119+
112120
// 更新最后保存时间
113121
ms.mutex.Lock()
114122
ms.lastSaveTime = time.Now()
@@ -228,3 +236,36 @@ func (ms *MetricsStorage) getLatencyDistributionMap() map[string]int64 {
228236
"gt1s": atomic.LoadInt64(&ms.collector.latencyBuckets.gt1s),
229237
}
230238
}
239+
240+
// getPathStatsArray 获取路径统计数组(转换为 sync.PathStat 格式)
241+
func (ms *MetricsStorage) getPathStatsArray() []sync.PathStat {
242+
pathMetrics := ms.collector.GetPathStats()
243+
if len(pathMetrics) == 0 {
244+
return nil
245+
}
246+
247+
now := time.Now().UnixMilli()
248+
result := make([]sync.PathStat, 0, len(pathMetrics))
249+
250+
for _, pm := range pathMetrics {
251+
result = append(result, sync.PathStat{
252+
Path: pm.Path,
253+
RequestCount: pm.RequestCount,
254+
ErrorCount: pm.ErrorCount,
255+
BytesTransferred: pm.BytesTransferred,
256+
Status2xx: pm.Status2xx,
257+
Status3xx: pm.Status3xx,
258+
Status4xx: pm.Status4xx,
259+
Status5xx: pm.Status5xx,
260+
CacheHits: pm.CacheHits,
261+
CacheMisses: pm.CacheMisses,
262+
CacheHitRate: pm.CacheHitRate,
263+
BytesSaved: pm.BytesSaved,
264+
AvgLatency: pm.AvgLatency,
265+
LastAccessTime: pm.LastAccessTime,
266+
UpdatedAt: now,
267+
})
268+
}
269+
270+
return result
271+
}

internal/service/config_service.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package service
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"net/url"
78
"proxy-go/internal/config"
9+
"proxy-go/pkg/sync"
10+
"time"
811
)
912

1013
type ConfigService struct {
@@ -52,6 +55,53 @@ func (s *ConfigService) SaveConfig(newConfig *config.Config) error {
5255
return nil
5356
}
5457

58+
// PullConfigFromD1 从 D1 拉取配置并更新本地
59+
func (s *ConfigService) PullConfigFromD1() ([]byte, error) {
60+
// 检查 D1 同步是否启用
61+
if !sync.IsEnabled() {
62+
return nil, fmt.Errorf("D1 同步未启用")
63+
}
64+
65+
// 从 D1 下载配置
66+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
67+
defer cancel()
68+
69+
configData, usedLocal, err := sync.DownloadConfigOnly(ctx)
70+
if err != nil {
71+
return nil, fmt.Errorf("从 D1 下载配置失败: %v", err)
72+
}
73+
74+
if usedLocal {
75+
return nil, fmt.Errorf("D1 配置为空,无法拉取")
76+
}
77+
78+
// 将 map[string]any 转换为 Config 结构
79+
configJson, err := json.Marshal(configData)
80+
if err != nil {
81+
return nil, fmt.Errorf("序列化配置失败: %v", err)
82+
}
83+
84+
var newConfig config.Config
85+
if err := json.Unmarshal(configJson, &newConfig); err != nil {
86+
return nil, fmt.Errorf("解析配置失败: %v", err)
87+
}
88+
89+
// 验证配置
90+
if err := s.validateConfig(&newConfig); err != nil {
91+
return nil, fmt.Errorf("配置验证失败: %v", err)
92+
}
93+
94+
// 更新本地配置
95+
if err := s.configManager.UpdateConfig(&newConfig); err != nil {
96+
return nil, fmt.Errorf("更新本地配置失败: %v", err)
97+
}
98+
99+
fmt.Printf("[Config] 从 D1 拉取配置成功: %d 个路径映射\n", len(newConfig.MAP))
100+
101+
// 返回配置数据
102+
return configJson, nil
103+
}
104+
55105
// validateConfig 验证配置
56106
func (s *ConfigService) validateConfig(cfg *config.Config) error {
57107
if cfg == nil {

pkg/sync/d1_manager.go

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ func (m *D1Manager) Start(ctx context.Context) error {
5656

5757
log.Printf("[D1Sync] Skipping initial sync at startup (already downloaded config)")
5858

59-
// 启动定时同步(仅同步config.json)
60-
go m.syncLoop(ctx)
59+
// 注意: 不再需要定时同步循环
60+
// - Config: 通过 ConfigUpdateCallback 在修改时立即同步
61+
// - Path Stats/Metrics: 由 MetricsStorage 每 30 分钟自动同步
62+
// - Banned IPs: 在封禁/解封时立即同步
6163

6264
return nil
6365
}
@@ -203,31 +205,6 @@ func (m *D1Manager) GetEventChannel() <-chan SyncEvent {
203205
return m.eventChan
204206
}
205207

206-
// syncLoop 同步循环
207-
func (m *D1Manager) syncLoop(ctx context.Context) {
208-
ticker := time.NewTicker(10 * time.Minute)
209-
defer ticker.Stop()
210-
211-
for {
212-
select {
213-
case <-ticker.C:
214-
if err := m.SyncNow(ctx); err != nil {
215-
log.Printf("[D1Sync] Scheduled sync failed: %v", err)
216-
m.sendEvent(SyncEvent{
217-
Type: SyncEventError,
218-
Timestamp: time.Now(),
219-
Message: "Scheduled sync failed",
220-
Error: err,
221-
})
222-
}
223-
case <-m.stopChan:
224-
return
225-
case <-ctx.Done():
226-
return
227-
}
228-
}
229-
}
230-
231208
// syncConfigToD1 同步配置到D1
232209
func (m *D1Manager) syncConfigToD1(ctx context.Context) error {
233210
config, err := m.configLoader.LoadConfig()
@@ -318,6 +295,20 @@ func (m *D1Manager) LoadLatencyDistribution(ctx context.Context) (map[string]int
318295
return result, nil
319296
}
320297

298+
// SavePathStats 保存路径统计到 D1
299+
func (m *D1Manager) SavePathStats(ctx context.Context, stats []PathStat) error {
300+
if len(stats) == 0 {
301+
return nil
302+
}
303+
304+
if err := m.storage.BatchUpsertPathStats(ctx, stats); err != nil {
305+
return fmt.Errorf("failed to save path stats: %w", err)
306+
}
307+
308+
log.Printf("[D1Sync] Saved %d path stats", len(stats))
309+
return nil
310+
}
311+
321312
// downloadConfigWithFallback 下载配置,如果远程不存在则上传本地配置
322313
// 返回: 远程配置数据(如果存在),是否使用了本地配置,错误
323314
func (m *D1Manager) downloadConfigWithFallback(ctx context.Context) (map[string]any, bool, error) {

pkg/sync/service.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,15 @@ func LoadPathStats(ctx context.Context) ([]PathStat, error) {
271271
// 调用 D1Client 的 GetPathStats 方法
272272
return globalSyncService.manager.storage.GetPathStats(ctx, "")
273273
}
274+
275+
// SavePathStats 保存路径统计到 D1
276+
func SavePathStats(ctx context.Context, stats []PathStat) error {
277+
globalSyncMutex.RLock()
278+
defer globalSyncMutex.RUnlock()
279+
280+
if globalSyncService == nil || !globalSyncService.isEnabled {
281+
return nil
282+
}
283+
284+
return globalSyncService.manager.SavePathStats(ctx, stats)
285+
}

web/app/dashboard/config/page.tsx

Lines changed: 28 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
66
import { useToast } from "@/components/ui/use-toast"
77
import { useRouter } from "next/navigation"
88
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
9-
import { Download, Upload } from "lucide-react"
9+
import { Download, RefreshCw } from "lucide-react"
1010
import {
1111
AlertDialog,
1212
AlertDialogAction,
@@ -624,84 +624,34 @@ export default function ConfigPage() {
624624
URL.revokeObjectURL(url)
625625
}
626626

627-
const importConfig = (event: React.ChangeEvent<HTMLInputElement>) => {
628-
const file = event.target.files?.[0]
629-
if (!file) return
630-
631-
const reader = new FileReader()
632-
reader.onload = (e) => {
633-
try {
634-
const content = e.target?.result as string
635-
const newConfig = JSON.parse(content)
636-
637-
// 验证配置结构
638-
if (!newConfig.MAP || typeof newConfig.MAP !== 'object') {
639-
throw new Error('配置文件缺少 MAP 字段或格式不正确')
640-
}
641-
642-
if (!newConfig.Compression ||
643-
typeof newConfig.Compression !== 'object' ||
644-
!newConfig.Compression.Gzip ||
645-
!newConfig.Compression.Brotli) {
646-
throw new Error('配置文件压缩设置格式不正确')
647-
}
627+
const pullConfigFromD1 = async () => {
628+
try {
629+
const response = await fetch('/admin/api/config/pull', {
630+
method: 'POST',
631+
})
648632

649-
// 如果没有安全配置,添加默认配置
650-
if (!newConfig.Security) {
651-
newConfig.Security = {
652-
IPBan: {
653-
Enabled: false,
654-
ErrorThreshold: 10,
655-
WindowMinutes: 5,
656-
BanDurationMinutes: 5,
657-
CleanupIntervalMinutes: 1
658-
}
659-
}
660-
}
633+
if (!response.ok) {
634+
const errorText = await response.text()
635+
throw new Error(errorText || '从 D1 拉取配置失败')
636+
}
661637

662-
// 验证路径映射
663-
for (const [path, target] of Object.entries(newConfig.MAP)) {
664-
if (!path.startsWith('/')) {
665-
throw new Error(`路径 ${path} 必须以/开头`)
666-
}
638+
const pulledConfig = await response.json()
667639

668-
if (typeof target === 'string') {
669-
try {
670-
new URL(target)
671-
} catch {
672-
throw new Error(`路径 ${path} 的目标URL格式不正确`)
673-
}
674-
} else if (target && typeof target === 'object') {
675-
const mapping = target as PathMapping
676-
if (!mapping.DefaultTarget || typeof mapping.DefaultTarget !== 'string') {
677-
throw new Error(`路径 ${path} 的默认目标格式不正确`)
678-
}
679-
try {
680-
new URL(mapping.DefaultTarget)
681-
} catch {
682-
throw new Error(`路径 ${path} 的默认目标URL格式不正确`)
683-
}
684-
} else {
685-
throw new Error(`路径 ${path} 的目标格式不正确`)
686-
}
687-
}
640+
// 使用 setConfig 而不是 updateConfig,因为拉取的配置已经在服务端更新过了
641+
isConfigFromApiRef.current = true
642+
setConfig(pulledConfig)
688643

689-
// 使用setConfig而不是updateConfig,因为导入的配置不应触发自动保存
690-
isConfigFromApiRef.current = true
691-
setConfig(newConfig)
692-
toast({
693-
title: "成功",
694-
description: "配置已导入",
695-
})
696-
} catch (error) {
697-
toast({
698-
title: "错误",
699-
description: error instanceof Error ? error.message : "配置文件格式错误",
700-
variant: "destructive",
701-
})
702-
}
644+
toast({
645+
title: "成功",
646+
description: "已从 D1 拉取最新配置",
647+
})
648+
} catch (error) {
649+
toast({
650+
title: "错误",
651+
description: error instanceof Error ? error.message : "从 D1 拉取配置失败",
652+
variant: "destructive",
653+
})
703654
}
704-
reader.readAsText(file)
705655
}
706656

707657
const handleEditPath = (path: string, target: PathMapping | string) => {
@@ -986,18 +936,10 @@ export default function ConfigPage() {
986936
<Download className="w-4 h-4 mr-2" />
987937
导出配置
988938
</Button>
989-
<label>
990-
<Button variant="outline" className="cursor-pointer">
991-
<Upload className="w-4 h-4 mr-2" />
992-
导入配置
993-
</Button>
994-
<input
995-
type="file"
996-
className="hidden"
997-
accept=".json"
998-
onChange={importConfig}
999-
/>
1000-
</label>
939+
<Button onClick={pullConfigFromD1} variant="outline">
940+
<RefreshCw className="w-4 h-4 mr-2" />
941+
从 D1 拉取配置
942+
</Button>
1001943
{saving && (
1002944
<div className="flex items-center text-sm text-muted-foreground">
1003945
<span className="animate-pulse mr-2"></span>

0 commit comments

Comments
 (0)