Skip to content

Commit e3471ac

Browse files
author
Ryan Mitchell
committed
fix(web): 同步主庫與 bot_configs,並補齊風控加固路徑
- 多處 UpdateConfig 後同步 bot_configs 快照與歷史來源 - 新人風控一鍵加固(post_risk_check_harden)納入全量同步 - 版本 3.105.0-rc5、CHANGELOG Made-with: Cursor
1 parent 7f5306c commit e3471ac

11 files changed

Lines changed: 137 additions & 9 deletions

CHANGELOG.md

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

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

5+
## [3.105.0-rc5] - 2026-04-15
6+
7+
### Fixed
8+
- **主庫 `app_config``bot_configs` 一致性**:除 **`PUT /api/bots/:id/strategy`** 外,補齊僅寫入主快照而未同步 Bot 文檔表的路徑——**創建/刪除 Bot、對沖組、批量 funding_carry、整體配置 JSON/YAML、AI 應用配置、新人風控一鍵加固、運行中風控持久化、開倉管理持久化** 等;刪除 Bot/組時 **`removeBotConfigSnapshotBestEffort`** 清理 `bot_configs` 行。
9+
10+
---
11+
512
## [3.105.0-rc4] - 2026-04-15
613

714
### 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-rc4"
48+
var Version = "3.105.0-rc5"
4949

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

web/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6359,6 +6359,7 @@ func applyAIConfig(c *gin.Context) {
63596359
respondError(c, http.StatusInternalServerError, "error.apply_config_failed", err)
63606360
return
63616361
}
6362+
syncAllBotConfigSnapshotsFromMain(cfg, "post_ai_apply_config")
63626363
SetGlobalConfig(cfg)
63636364
if configHotReloader != nil {
63646365
_, _ = configHotReloader.UpdateConfig(cfg)

web/api_bot_config.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package web
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"strings"
@@ -16,6 +17,9 @@ import (
1617

1718
var botConfigManager *config.BotConfigManager
1819

20+
// errBotConfigSnapshotNotFound deleteBotConfigUnified 在無可刪文檔時返回,供調用方忽略。
21+
var errBotConfigSnapshotNotFound = errors.New("bot config snapshot not found")
22+
1923
// InitBotConfigManager 初始化 Bot 配置文件管理器(僅作無主庫時的後備;權威持久化為 bot_configs 表)
2024
func InitBotConfigManager(baseDir string) error {
2125
var err error
@@ -113,19 +117,52 @@ func resolveBotConfigFileFromUnifiedOrMain(botID string) (*config.BotConfigFile,
113117
}
114118

115119
// syncBotConfigSnapshotFromMainBot 將主配置中的單個 Bot 寫入 bot_configs(與 app_config 對齊)。
116-
// 用於僅調用 fileConfigManager.UpdateConfig 的路徑(如 PUT /api/bots/:id/strategy),避免主庫快照與 bot_configs 文檔不一致。
117-
func syncBotConfigSnapshotFromMainBot(botID string, bc *config.BotConfig) error {
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 {
118123
if bc == nil || !botConfigStorageReady() {
119124
return nil
120125
}
126+
if historySource == "" {
127+
historySource = "sync_bot_config_snapshot"
128+
}
121129
bf := config.ConvertFromBotConfig(*bc)
122130
bf.UpdatedAt = time.Now().Format(time.RFC3339)
123131
if prev, err := loadBotConfigUnified(botID); err == nil && prev != nil && prev.CreatedAt != "" {
124132
bf.CreatedAt = prev.CreatedAt
125133
} else if bc.CreatedAt != "" {
126134
bf.CreatedAt = bc.CreatedAt
127135
}
128-
return saveBotConfigUnified(bf, "web", "put_bot_strategy")
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+
158+
// removeBotConfigSnapshotBestEffort 刪除 bot_configs 行(刪除 Bot 時調用);無行則忽略。
159+
func removeBotConfigSnapshotBestEffort(botID string) {
160+
if botID == "" || !botConfigStorageReady() {
161+
return
162+
}
163+
if err := deleteBotConfigUnified(botID); err != nil && !errors.Is(err, errBotConfigSnapshotNotFound) {
164+
logger.Warn("刪除 bot_configs 失敗 (%s): %v", botID, err)
165+
}
129166
}
130167

131168
func saveBotConfigUnified(bf *config.BotConfigFile, operator, source string) error {
@@ -169,7 +206,7 @@ func deleteBotConfigUnified(botID string) error {
169206
deleted = true
170207
}
171208
if !deleted {
172-
return fmt.Errorf("not found")
209+
return errBotConfigSnapshotNotFound
173210
}
174211
return nil
175212
}

web/api_bot_risk_control.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +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-
return fileConfigManager.UpdateConfig(cfg)
287+
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
288+
return err
289+
}
290+
return syncBotConfigSnapshotFromMainBot(botID, &cfg.Bots[i], "put_bot_risk_control_running")
288291
}
289292
}
290293
return nil
@@ -306,7 +309,10 @@ func persistGridRiskControlToConfig(botID string, grc config.GridRiskControl) er
306309
}
307310
if id == botID {
308311
cfg.Bots[i].GridRiskControl = grc
309-
return fileConfigManager.UpdateConfig(cfg)
312+
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
313+
return err
314+
}
315+
return syncBotConfigSnapshotFromMainBot(botID, &cfg.Bots[i], "put_bot_risk_control_running")
310316
}
311317
}
312318
return nil

web/api_bots.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,12 @@ func postBotCreate(c *gin.Context) {
477477
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
478478
return
479479
}
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)
483+
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
484+
return
485+
}
480486
c.JSON(http.StatusOK, gin.H{"ok": true, "bot_id": botID})
481487
}
482488

@@ -622,13 +628,31 @@ func deleteBot(c *gin.Context) {
622628
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
623629
return
624630
}
631+
removeBotConfigSnapshotBestEffort(botID)
625632
if botManagerProvider != nil {
626633
_ = botManagerProvider.StopBot(botID)
627634
}
628635
logger.Info("✅ [Bot刪除] 已移除 %s", botID)
629636
c.JSON(http.StatusOK, gin.H{"ok": true, "bot_id": botID})
630637
}
631638

639+
// botCfgByID 在主配置快照中按 ID 查找 Bot(與 GenerateBotID 回退規則一致)。
640+
func botCfgByID(cfg *config.Config, botID string) *config.BotConfig {
641+
if cfg == nil || botID == "" {
642+
return nil
643+
}
644+
for i := range cfg.Bots {
645+
id := cfg.Bots[i].ID
646+
if id == "" {
647+
id = config.GenerateBotID(cfg.Bots[i].Exchange, cfg.Bots[i].Symbol, cfg.Bots[i].GetMarketType())
648+
}
649+
if id == botID {
650+
return &cfg.Bots[i]
651+
}
652+
}
653+
return nil
654+
}
655+
632656
// postBotStop 停止 Bot
633657
// POST /api/bots/:id/stop
634658
func postBotStop(c *gin.Context) {
@@ -1072,6 +1096,16 @@ func postBotGroupCreate(c *gin.Context) {
10721096
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
10731097
return
10741098
}
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)
1106+
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
1107+
return
1108+
}
10751109
c.JSON(http.StatusOK, gin.H{"ok": true, "group_id": groupID, "bot_ids": []string{futuresID, spotID}})
10761110
}
10771111

@@ -1158,6 +1192,9 @@ func deleteBotGroup(c *gin.Context) {
11581192
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
11591193
return
11601194
}
1195+
for _, id := range botIDsToRemove {
1196+
removeBotConfigSnapshotBestEffort(id)
1197+
}
11611198
c.JSON(http.StatusOK, gin.H{"ok": true, "group_id": groupID})
11621199
}
11631200

@@ -1349,7 +1386,7 @@ func putBotStrategy(c *gin.Context) {
13491386
if id != botID {
13501387
continue
13511388
}
1352-
if err := syncBotConfigSnapshotFromMainBot(botID, &cfg.Bots[i]); err != nil {
1389+
if err := syncBotConfigSnapshotFromMainBot(botID, &cfg.Bots[i], "put_bot_strategy"); err != nil {
13531390
logger.Error("同步 bot_configs 失敗 (bot_id=%s): %v", botID, err)
13541391
respondError(c, http.StatusInternalServerError, "error.config_save_failed", err)
13551392
return

web/api_config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,8 @@ func updateConfigHandler(c *gin.Context) {
482482
return
483483
}
484484

485+
syncAllBotConfigSnapshotsFromMain(newConfig, "post_config_update")
486+
485487
// 尝試热更新
486488
if configHotReloader != nil {
487489
_, err := configHotReloader.UpdateConfig(newConfig)
@@ -585,6 +587,8 @@ func updateConfigYAMLHandler(c *gin.Context) {
585587
return
586588
}
587589

590+
syncAllBotConfigSnapshotsFromMain(newConfig, "post_config_update_yaml")
591+
588592
// 尝試热更新
589593
if configHotReloader != nil {
590594
_, _ = configHotReloader.UpdateConfig(newConfig)

web/api_funding_carry.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,25 @@ func postBatchCreateFunding(c *gin.Context) {
109109
respondError(c, http.StatusInternalServerError, "error.config_save_failed")
110110
return
111111
}
112+
for _, sym := range created {
113+
for i := range cfg.Bots {
114+
b := &cfg.Bots[i]
115+
if b.MarketType != config.MarketTypeFundingCarry {
116+
continue
117+
}
118+
if !strings.EqualFold(b.Exchange, req.Exchange) || b.Symbol != sym {
119+
continue
120+
}
121+
bid := b.ID
122+
if bid == "" {
123+
bid = config.GenerateBotID(b.Exchange, b.Symbol, b.GetMarketType())
124+
}
125+
if err := syncBotConfigSnapshotFromMainBot(bid, b, "post_funding_carry_batch"); err != nil {
126+
logger.Error("同步 bot_configs 失敗 funding_carry %s: %v", bid, err)
127+
}
128+
break
129+
}
130+
}
112131

113132
if botManagerProvider != nil {
114133
for _, bc := range cfg.Bots {

web/api_opening_control.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ func putOpeningControlConfig(c *gin.Context) {
288288
}
289289

290290
persisted := false
291+
var syncedBotID string
291292
botIDQ := strings.TrimSpace(c.Query("bot_id"))
292293
if botIDQ != "" {
293294
for i := range cfg.Bots {
@@ -297,6 +298,10 @@ func putOpeningControlConfig(c *gin.Context) {
297298
b.OpenPositionControl.MaxPositionLayers = req.MaxPositionLayers
298299
b.OpenPositionControl.ScheduleRules = req.ScheduleRules
299300
b.OpenPositionControl.PeriodicRule = req.PeriodicRule
301+
syncedBotID = b.ID
302+
if syncedBotID == "" {
303+
syncedBotID = config.GenerateBotID(b.Exchange, b.Symbol, b.GetMarketType())
304+
}
300305
persisted = true
301306
break
302307
}
@@ -315,6 +320,10 @@ func putOpeningControlConfig(c *gin.Context) {
315320
b.OpenPositionControl.MaxPositionLayers = req.MaxPositionLayers
316321
b.OpenPositionControl.ScheduleRules = req.ScheduleRules
317322
b.OpenPositionControl.PeriodicRule = req.PeriodicRule
323+
syncedBotID = b.ID
324+
if syncedBotID == "" {
325+
syncedBotID = config.GenerateBotID(b.Exchange, b.Symbol, b.GetMarketType())
326+
}
318327
persisted = true
319328
break
320329
}
@@ -345,6 +354,12 @@ func putOpeningControlConfig(c *gin.Context) {
345354
if persisted {
346355
if err := fileConfigManager.UpdateConfig(cfg); err != nil {
347356
logger.Warn("⚠️ [開倉管理] 配置持久化失敗: %v", err)
357+
} else if syncedBotID != "" {
358+
if b := botCfgByID(cfg, syncedBotID); b != nil {
359+
if err := syncBotConfigSnapshotFromMainBot(syncedBotID, b, "put_opening_control"); err != nil {
360+
logger.Warn("⚠️ [開倉管理] 同步 bot_configs 失敗 (%s): %v", syncedBotID, err)
361+
}
362+
}
348363
}
349364
} else if !ok {
350365
respondError(c, http.StatusNotFound, "error.symbol_not_found")

web/api_risk_check.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ func applyNewbieSecurityConfig(c *gin.Context) {
295295
return
296296
}
297297

298+
syncAllBotConfigSnapshotsFromMain(&newConfig, "post_risk_check_harden")
299+
298300
// 热更新
299301
if configHotReloader != nil {
300302
configHotReloader.UpdateConfig(&newConfig)

0 commit comments

Comments
 (0)