Skip to content

Commit c28d279

Browse files
author
Ryan Mitchell
committed
fix(storage): SaveAppConfigSnapshot 同步 bot_configs;Web 統一走 WithBotSource
- 新增 SyncBotConfigSnapshotsFromMainConfig、SaveAppConfigSnapshotWithBotSource - FileConfigManager.UpdateConfigWithBotHistorySource 與 persistAppConfigToDB 擴展 - 移除重複的 sync 輔助函數;補單元測試 - 版本 3.105.0-rc6、CHANGELOG Made-with: Cursor
1 parent e3471ac commit c28d279

13 files changed

Lines changed: 174 additions & 129 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
所有重要的專案更新都會記錄在此檔案中。
44

5+
## [3.105.0-rc6] - 2026-04-15
6+
7+
### Fixed
8+
- **體系外持久化****`storage.SaveAppConfigSnapshot`** 在寫入 **`app_config`** 後同步 **`cfg.Bots``bot_configs`****`SaveAppConfigSnapshotWithBotSource`** 可單獨指定 **`bot_config_history.source`**(與 **`app_config_history`**`file_config_update` 區分審計)。**`main.go` 首次快照****`--migrate-app-config`**(原本已在一事務內寫入 YAML 目錄 Bot,行為不變)與 Web **`UpdateConfigWithBotHistorySource`** 路徑一致。
9+
- **Web**:移除重複的 **`sync*`** 輔助函數,改由 **`persistAppConfigToDB`** 統一觸發存儲層同步。
10+
11+
### Added
12+
- **`storage` 單元測試**`TestSaveAppConfigSnapshotSyncsBotConfigs`
13+
14+
---
15+
516
## [3.105.0-rc5] - 2026-04-15
617

718
### Fixed

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import (
4545
)
4646

4747
// Version 应用版本号
48-
var Version = "3.105.0-rc5"
48+
var Version = "3.105.0-rc6"
4949

5050
// capitalDataSourceAdapter 资金數據源适配器
5151
type capitalDataSourceAdapter struct {

storage/app_config_document.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,16 +330,68 @@ func DeleteBotConfigSnapshot(ctx context.Context, st Storage, botID string) erro
330330
return err
331331
}
332332

333-
// SaveAppConfigSnapshot 將完整主配置 JSON 寫入 app_config 並追加 app_config_history(與 MigrateYAMLToAppConfigDB 入庫一致)。
333+
// SyncBotConfigSnapshotsFromMainConfig 將主配置中的 cfg.Bots 逐條寫入 bot_configs(與 app_config 快照對齊)。
334+
// 用於 SaveAppConfigSnapshot、main 引導等僅寫入主快照的路徑,避免與 bot_configs 文檔表不一致。
335+
func SyncBotConfigSnapshotsFromMainConfig(ctx context.Context, st Storage, cfg *config.Config, operator, source string) error {
336+
if cfg == nil || len(cfg.Bots) == 0 {
337+
return nil
338+
}
339+
ss, ok := st.(*SQLStorage)
340+
if !ok || ss == nil {
341+
return nil
342+
}
343+
if err := ss.EnsureAppConfigDocumentTables(); err != nil {
344+
return err
345+
}
346+
for i := range cfg.Bots {
347+
bc := &cfg.Bots[i]
348+
id := strings.TrimSpace(bc.ID)
349+
if id == "" {
350+
id = config.GenerateBotID(bc.Exchange, bc.Symbol, bc.GetMarketType())
351+
}
352+
bf := config.ConvertFromBotConfig(*bc)
353+
bf.UpdatedAt = time.Now().Format(time.RFC3339)
354+
if doc, err := ss.GetBotConfigDocument(ctx, id); err == nil && doc != nil && strings.TrimSpace(doc.Content) != "" {
355+
var prev config.BotConfigFile
356+
if json.Unmarshal([]byte(doc.Content), &prev) == nil && prev.CreatedAt != "" {
357+
bf.CreatedAt = prev.CreatedAt
358+
}
359+
} else if bc.CreatedAt != "" {
360+
bf.CreatedAt = bc.CreatedAt
361+
}
362+
if _, err := SaveBotConfigSnapshot(ctx, st, bf, operator, source); err != nil {
363+
return fmt.Errorf("sync bot_configs %s: %w", id, err)
364+
}
365+
}
366+
return nil
367+
}
368+
369+
// SaveAppConfigSnapshot 將完整主配置 JSON 寫入 app_config 並追加 app_config_history,並同步 cfg.Bots 至 bot_configs。
334370
func SaveAppConfigSnapshot(ctx context.Context, st Storage, cfg *config.Config, operator, source string) (revision int, err error) {
371+
return SaveAppConfigSnapshotWithBotSource(ctx, st, cfg, operator, source, "")
372+
}
373+
374+
// SaveAppConfigSnapshotWithBotSource 同上;若 botHistorySource 為空,bot_config_history.source 使用 appSource,否則使用 botHistorySource。
375+
func SaveAppConfigSnapshotWithBotSource(ctx context.Context, st Storage, cfg *config.Config, operator, appSource, botHistorySource string) (revision int, err error) {
335376
if st == nil || cfg == nil {
336377
return 0, fmt.Errorf("SaveAppConfigSnapshot: storage 或配置為空")
337378
}
338379
jsonBytes, err := json.Marshal(cfg)
339380
if err != nil {
340381
return 0, fmt.Errorf("序列化配置為 JSON: %w", err)
341382
}
342-
return SaveAppConfigSnapshotFromJSON(ctx, st, jsonBytes, operator, source)
383+
rev, err := SaveAppConfigSnapshotFromJSON(ctx, st, jsonBytes, operator, appSource)
384+
if err != nil {
385+
return 0, err
386+
}
387+
botSrc := botHistorySource
388+
if strings.TrimSpace(botSrc) == "" {
389+
botSrc = appSource
390+
}
391+
if err := SyncBotConfigSnapshotsFromMainConfig(ctx, st, cfg, operator, botSrc); err != nil {
392+
return rev, err
393+
}
394+
return rev, nil
343395
}
344396

345397
// SaveAppConfigSnapshotFromJSON 將主配置 JSON 寫入 app_config(可含 config.Config 結構體未涵蓋的鍵,例如 security)。

storage/app_config_document_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql"
66
"os"
77
"path/filepath"
8+
"strings"
89
"testing"
910

1011
"quantmesh/config"
@@ -180,3 +181,74 @@ trading:
180181
t.Fatalf("expected DB fee 0.0005, got %v", cfgPtr.Exchanges["binance"].FeeRate)
181182
}
182183
}
184+
185+
// SaveAppConfigSnapshot 應將 cfg.Bots 同步寫入 bot_configs(與 main 引導、Web 持久化一致)。
186+
func TestSaveAppConfigSnapshotSyncsBotConfigs(t *testing.T) {
187+
dir := t.TempDir()
188+
dbPath := filepath.Join(dir, "t.db")
189+
st, err := NewSQLStorage(dbPath)
190+
if err != nil {
191+
t.Fatal(err)
192+
}
193+
defer st.Close()
194+
195+
yamlPath := filepath.Join(dir, "c.yaml")
196+
minimal := `app:
197+
current_exchange: binance
198+
exchanges:
199+
binance:
200+
api_key: "k"
201+
secret_key: "s"
202+
trading:
203+
symbols:
204+
- symbol: BTCUSDT
205+
exchange: binance
206+
price_interval: 100
207+
order_quantity: 10
208+
buy_window_size: 5
209+
sell_window_size: 5
210+
`
211+
if err := os.WriteFile(yamlPath, []byte(minimal), 0644); err != nil {
212+
t.Fatal(err)
213+
}
214+
t.Setenv("QUANTMESH_MIGRATE_APP_CONFIG_FORCE", "1")
215+
if _, err := MigrateYAMLToAppConfigDB(context.Background(), st, yamlPath, filepath.Join(dir, "nobots"), MigrateYAMLModeCLI); err != nil {
216+
t.Fatal(err)
217+
}
218+
cfg, err := loadConfigFromAppConfigDocument(st)
219+
if err != nil || cfg == nil {
220+
t.Fatalf("load: %v cfg=%v", err, cfg)
221+
}
222+
bid := "sync-test-bot"
223+
enb := true
224+
cfg.Bots = []config.BotConfig{{
225+
ID: bid,
226+
Name: "x",
227+
Exchange: "binance",
228+
Symbol: "BTCUSDT",
229+
MarketType: "futures",
230+
Enabled: &enb,
231+
Strategies: []config.StrategyInstance{{Type: "grid", Weight: 1}},
232+
PriceInterval: 100,
233+
OrderQuantity: 10,
234+
MinOrderValue: 20,
235+
BuyWindowSize: 5,
236+
SellWindowSize: 5,
237+
ReconcileInterval: 60,
238+
OrderCleanupThreshold: 50,
239+
CleanupBatchSize: 10,
240+
MarginLockDurationSec: 10,
241+
PositionSafetyCheck: config.DefaultPositionSafetyCheck,
242+
Direction: "LONG",
243+
}}
244+
if _, err := SaveAppConfigSnapshot(context.Background(), st, cfg, "test", "save_app_snapshot"); err != nil {
245+
t.Fatal(err)
246+
}
247+
doc, err := st.GetBotConfigDocument(context.Background(), bid)
248+
if err != nil {
249+
t.Fatal(err)
250+
}
251+
if doc == nil || strings.TrimSpace(doc.Content) == "" {
252+
t.Fatalf("expected bot_configs row for %s", bid)
253+
}
254+
}

web/api.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6354,12 +6354,11 @@ func applyAIConfig(c *gin.Context) {
63546354
respondError(c, http.StatusInternalServerError, "error.config_manager_unavailable")
63556355
return
63566356
}
6357-
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
6357+
if err := fileConfigManager.UpdateConfigWithBotHistorySource(cfg, "post_ai_apply_config"); err != nil {
63586358
logger.Error("❌ 持久化 AI 配置失败: %v", err)
63596359
respondError(c, http.StatusInternalServerError, "error.apply_config_failed", err)
63606360
return
63616361
}
6362-
syncAllBotConfigSnapshotsFromMain(cfg, "post_ai_apply_config")
63636362
SetGlobalConfig(cfg)
63646363
if configHotReloader != nil {
63656364
_, _ = configHotReloader.UpdateConfig(cfg)

web/api_bot_config.go

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -116,45 +116,6 @@ func resolveBotConfigFileFromUnifiedOrMain(botID string) (*config.BotConfigFile,
116116
return nil, nil
117117
}
118118

119-
// syncBotConfigSnapshotFromMainBot 將主配置中的單個 Bot 寫入 bot_configs(與 app_config 對齊)。
120-
// 用於僅調用 fileConfigManager.UpdateConfig 的路徑,避免主庫快照與 bot_configs 文檔不一致。
121-
// historySource 寫入 bot_config_history.source,便於審計(如 put_bot_strategy、post_config_update)。
122-
func syncBotConfigSnapshotFromMainBot(botID string, bc *config.BotConfig, historySource string) error {
123-
if bc == nil || !botConfigStorageReady() {
124-
return nil
125-
}
126-
if historySource == "" {
127-
historySource = "sync_bot_config_snapshot"
128-
}
129-
bf := config.ConvertFromBotConfig(*bc)
130-
bf.UpdatedAt = time.Now().Format(time.RFC3339)
131-
if prev, err := loadBotConfigUnified(botID); err == nil && prev != nil && prev.CreatedAt != "" {
132-
bf.CreatedAt = prev.CreatedAt
133-
} else if bc.CreatedAt != "" {
134-
bf.CreatedAt = bc.CreatedAt
135-
}
136-
return saveBotConfigUnified(bf, "web", historySource)
137-
}
138-
139-
// syncAllBotConfigSnapshotsFromMain 將 app_config.Bots 逐條同步到 bot_configs(整份主配置替換、AI 應用等)。
140-
func syncAllBotConfigSnapshotsFromMain(cfg *config.Config, historySource string) {
141-
if cfg == nil || !botConfigStorageReady() {
142-
return
143-
}
144-
if historySource == "" {
145-
historySource = "sync_all_bot_configs"
146-
}
147-
for i := range cfg.Bots {
148-
id := cfg.Bots[i].ID
149-
if id == "" {
150-
id = config.GenerateBotID(cfg.Bots[i].Exchange, cfg.Bots[i].Symbol, cfg.Bots[i].GetMarketType())
151-
}
152-
if err := syncBotConfigSnapshotFromMainBot(id, &cfg.Bots[i], historySource); err != nil {
153-
logger.Error("同步 bot_configs 失敗 bot_id=%s: %v", id, err)
154-
}
155-
}
156-
}
157-
158119
// removeBotConfigSnapshotBestEffort 刪除 bot_configs 行(刪除 Bot 時調用);無行則忽略。
159120
func removeBotConfigSnapshotBestEffort(botID string) {
160121
if botID == "" || !botConfigStorageReady() {

web/api_bot_risk_control.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,10 @@ func persistBotRiskControlToConfig(botID string, rc config.BotRiskControl) error
284284
cfg.Bots[i].OpenPositionControl.BotRiskControl = &config.BotRiskControl{}
285285
}
286286
*cfg.Bots[i].OpenPositionControl.BotRiskControl = rc
287-
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
287+
if err := fileConfigManager.UpdateConfigWithBotHistorySource(cfg, "put_bot_risk_control_running"); err != nil {
288288
return err
289289
}
290-
return syncBotConfigSnapshotFromMainBot(botID, &cfg.Bots[i], "put_bot_risk_control_running")
290+
return nil
291291
}
292292
}
293293
return nil
@@ -309,10 +309,10 @@ func persistGridRiskControlToConfig(botID string, grc config.GridRiskControl) er
309309
}
310310
if id == botID {
311311
cfg.Bots[i].GridRiskControl = grc
312-
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
312+
if err := fileConfigManager.UpdateConfigWithBotHistorySource(cfg, "put_bot_risk_control_running"); err != nil {
313313
return err
314314
}
315-
return syncBotConfigSnapshotFromMainBot(botID, &cfg.Bots[i], "put_bot_risk_control_running")
315+
return nil
316316
}
317317
}
318318
return nil

web/api_bots.go

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -473,13 +473,7 @@ func postBotCreate(c *gin.Context) {
473473
}
474474

475475
cfg.Bots = append(cfg.Bots, bc)
476-
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
477-
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
478-
return
479-
}
480-
last := &cfg.Bots[len(cfg.Bots)-1]
481-
if err := syncBotConfigSnapshotFromMainBot(botID, last, "post_bot_create"); err != nil {
482-
logger.Error("同步 bot_configs 失敗 (post_bot_create %s): %v", botID, err)
476+
if err := fileConfigManager.UpdateConfigWithBotHistorySource(cfg, "post_bot_create"); err != nil {
483477
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
484478
return
485479
}
@@ -1092,17 +1086,7 @@ func postBotGroupCreate(c *gin.Context) {
10921086
cfg.BotGroups = append(cfg.BotGroups, group)
10931087
cfg.Bots = append(cfg.Bots, botsToAppend...)
10941088

1095-
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
1096-
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
1097-
return
1098-
}
1099-
if err := syncBotConfigSnapshotFromMainBot(spotID, botCfgByID(cfg, spotID), "post_bot_group_create"); err != nil {
1100-
logger.Error("同步 bot_configs 失敗 (spot %s): %v", spotID, err)
1101-
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
1102-
return
1103-
}
1104-
if err := syncBotConfigSnapshotFromMainBot(futuresID, botCfgByID(cfg, futuresID), "post_bot_group_create"); err != nil {
1105-
logger.Error("同步 bot_configs 失敗 (futures %s): %v", futuresID, err)
1089+
if err := fileConfigManager.UpdateConfigWithBotHistorySource(cfg, "post_bot_group_create"); err != nil {
11061090
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
11071091
return
11081092
}
@@ -1372,28 +1356,11 @@ func putBotStrategy(c *gin.Context) {
13721356
return
13731357
}
13741358

1375-
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
1359+
if err := fileConfigManager.UpdateConfigWithBotHistorySource(cfg, "put_bot_strategy"); err != nil {
13761360
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
13771361
return
13781362
}
13791363

1380-
// 同步主庫 bot_configs,與 app_config 快照一致(否則啟動時優先讀 bot_configs 仍為舊方向/參數)
1381-
for i := range cfg.Bots {
1382-
id := cfg.Bots[i].ID
1383-
if id == "" {
1384-
id = config.GenerateBotID(cfg.Bots[i].Exchange, cfg.Bots[i].Symbol, cfg.Bots[i].GetMarketType())
1385-
}
1386-
if id != botID {
1387-
continue
1388-
}
1389-
if err := syncBotConfigSnapshotFromMainBot(botID, &cfg.Bots[i], "put_bot_strategy"); err != nil {
1390-
logger.Error("同步 bot_configs 失敗 (bot_id=%s): %v", botID, err)
1391-
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
1392-
return
1393-
}
1394-
break
1395-
}
1396-
13971364
// 推送配置到運行中的 Bot,確保 smart_order 等變更在刷新頁面時正確顯示
13981365
if symbolManagerProvider != nil {
13991366
if updater, ok := symbolManagerProvider.(TradingParamsUpdater); ok {

0 commit comments

Comments
 (0)