package applogic import ( "context" "fmt" "log" "strings" "sync" "aijianzhan/platform/internal/agentstore" "aijianzhan/platform/internal/dbsync" "aijianzhan/platform/internal/svc" ) // HealResult Z12h 自愈结果。 type HealResult struct { ChannelID string `json:"channel_id"` BindingsFixed int `json:"bindings_fixed"` AgentsFixed int `json:"agents_fixed"` ChannelCreated bool `json:"channel_created,omitempty"` } var healTenantMu sync.Map // tenantID -> *sync.Mutex;避免 SyncPage 并行列表双建默认通道 func lockTenantHeal(tenantID int64) func() { v, _ := healTenantMu.LoadOrStore(tenantID, &sync.Mutex{}) mu := v.(*sync.Mutex) mu.Lock() return mu.Unlock } // channelAlive 通道是否仍属于该租户。 func channelAlive(store *dbsync.FileStore, tenantID int64, channelID string) bool { id := strings.TrimSpace(channelID) if id == "" || store == nil || tenantID <= 0 { return false } _, err := store.GetChannelForTenant(id, tenantID) return err == nil } func agentNeedsSyncHeal(store *dbsync.FileStore, a agentstore.Account) bool { cid := strings.TrimSpace(a.ChannelID) alive := channelAlive(store, a.TenantID, cid) if cid != "" { if !alive { return true // 挂死通道 } return strings.TrimSpace(a.OnlineDBID) == "" // 通道在但缺 online } // 无通道:仅 active(与 Z12c 启用即绑一致),勿动 pending return a.Status == agentstore.StatusActive || a.Status == "" } // HealTenantSyncBind Z12h:确保公司默认同步通道存在并运行;重挂挂死通道的 Agent/Binding。 // online_db_id 有值则保留(同一 remote 上的逻辑库名不变);仅改 channel_id。 func HealTenantSyncBind(ctx context.Context, svcCtx *svc.ServiceContext, tenantID int64) (*HealResult, error) { if svcCtx == nil || svcCtx.DBSync == nil || tenantID <= 0 { return nil, nil } unlock := lockTenantHeal(tenantID) defer unlock() cfg := svcCtx.Config.DBSync driver := dbsync.Driver(strings.TrimSpace(cfg.DefaultRemoteDriver)) if driver == "" { driver = dbsync.DriverPostgres } before, _ := svcCtx.DBSync.Store().FindSystemDefaultChannel(tenantID) ch, err := svcCtx.DBSync.EnsureAndStartSystemDefaultChannel(dbsync.DefaultChannelOpts{ TenantID: tenantID, RemoteDriver: driver, RemoteDSN: strings.TrimSpace(cfg.DefaultRemoteDSN), Name: fmt.Sprintf("公司默认同步 #%d", tenantID), }) if err != nil { return nil, err } out := &HealResult{ ChannelID: ch.ID, ChannelCreated: before == nil || before.ID != ch.ID, } // Binding:通道空或已删除 → 改挂默认通道(保留 online_db_id) list, err := svcCtx.DBSync.Store().ListBindings(tenantID, "") if err == nil { for _, b := range list { if channelAlive(svcCtx.DBSync.Store(), tenantID, b.ChannelID) { continue } b.ChannelID = ch.ID if strings.TrimSpace(b.OnlineDBID) == "" { b.OnlineDBID = dbsync.ResolveOnlineDBID("", ch.ID) } if _, e := svcCtx.DBSync.Store().EnsureBinding(b); e != nil { log.Printf("dbsync heal binding tenant=%d local=%s: %v", tenantID, b.LocalDatabaseID, e) continue } out.BindingsFixed++ } } // Agent:挂死通道 / 缺 online / active 无通道 → AttachSyncBind(保留 online_db_id) if svcCtx.Agents != nil { agents, err := svcCtx.Agents.List(ctx, tenantID) if err == nil { for i := range agents { a := agents[i] if !agentNeedsSyncHeal(svcCtx.DBSync.Store(), a) { continue } online := strings.TrimSpace(a.OnlineDBID) if online == "" { online = dbsync.ResolveOnlineDBID("", ch.ID) } dbName := strings.TrimSpace(a.DatabaseName) if dbName == "" { dbName = fmt.Sprintf("agent_%d", a.AgentID) } if _, e := svcCtx.Agents.AttachSyncBind(ctx, a.AgentID, tenantID, ch.ID, online, dbName, a.Status == agentstore.StatusActive || a.Status == ""); e != nil { log.Printf("dbsync heal agent tenant=%d agent=%d: %v", tenantID, a.AgentID, e) continue } out.AgentsFixed++ } } } if out.BindingsFixed > 0 || out.AgentsFixed > 0 || out.ChannelCreated { log.Printf("dbsync heal tenant=%d channel=%s bindings=%d agents=%d created=%v", tenantID, ch.ID, out.BindingsFixed, out.AgentsFixed, out.ChannelCreated) } return out, nil }