Ensure default channel and rebind Agent/Binding on list, ticket, agents/me, and ensure-binding so SyncPage does not stay on deleted-channel errors. Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
4.2 KiB
Go
132 lines
4.2 KiB
Go
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
|
||
}
|