package console import ( "fmt" "sort" "strings" "yunyan/comm" "yunyan/lego/sys/log" "yunyan/lego/sys/postgres" ) // ============================ 作用域归一:全局层 → 每个应用一套 ============================ // // 2026-09-14 之前第三方服务配置是「全局默认(app_name='') + 应用覆盖」:应用没配的服务回退到全局行。 // 两台机上的全部服务/区域分叉/会议编排/会议模板都在全局层,没有一个应用真正有自己的一套。 // 按要求改成**每个应用一套、互不干涉**:应用 A 删掉某个服务,不会悄悄回退到别人那份。 // // 迁移把全局层的行分给每个已启用的应用: // - **第一个应用(app_registry 里 id 最小的)直接把 app_name 从 '' 改成它**,行本身不动—— // 行 id/row_id 全部保留,svc_health、echomeet_record 等按 id 引用的数据不受影响; // - 其余应用各拷一份(新 id),已存在的 (app, key) 不覆盖。 // 跑完之后全局层为空,运行时那些 `OR app_name=''` 的回退分支再也匹配不到任何行。 // // 幂等:全局层空了就整体跳过。每次启动跑。 // // ⚠️ 作用域键是 app_registry.**app_name**(= 应用中心的应用名,如 EAIMAR / deepGlass),不是 // app_registry.name(部署名,如 deepglass)。运行时用 ANALYZE_APP_NAME 找自己的行, // 实测它等于 app_name;用错列会让 deepGlass 一个服务都读不到。 // // 涉及的表(都以 app_name 为作用域键): // svc_config / svc_region_override / echomeet_orch / echomeet_orch_setting / echomeet_template(source=public) func migrateGlobalScopeToApps(apps []*AppRegistry) { targets := scopeTargetApps(apps) if len(targets) == 0 { log.Infof("console.scope: 没有已启用的应用,全局层原样保留") return } first, rest := targets[0], targets[1:] n1, err := migrateScopeSvcConfig(first, rest) if err != nil { log.Warnf("console.scope: svc_config 归一失败: %v", err) } n2, err := migrateScopeRegionOverride(first, rest) if err != nil { log.Warnf("console.scope: svc_region_override 归一失败: %v", err) } n3, err := migrateScopeEchoOrch(first, rest) if err != nil { log.Warnf("console.scope: echomeet_orch 归一失败: %v", err) } n4, err := migrateScopeEchoOrchSetting(first, rest) if err != nil { log.Warnf("console.scope: echomeet_orch_setting 归一失败: %v", err) } n5, err := migrateScopeMeetTemplate(first, rest) if err != nil { log.Warnf("console.scope: echomeet_template 归一失败: %v", err) } if n1+n2+n3+n4+n5 > 0 { log.Infof("console.scope: 全局层已分给各应用(首个应用 %s 原行改名,其余 %v 各拷一份):"+ "svc_config %d / 区域分叉 %d / 会议编排 %d / 编排开关 %d / 会议模板 %d", first, rest, n1, n2, n3, n4, n5) } } // scopeTargetApps 已启用应用的 app_name 去重,按 app_registry.id 升序(决定谁拿原行)。 func scopeTargetApps(apps []*AppRegistry) []string { sort.Slice(apps, func(i, j int) bool { return apps[i].Id < apps[j].Id }) seen := map[string]bool{} out := make([]string, 0, len(apps)) for _, a := range apps { n := strings.TrimSpace(a.AppName) if !a.Enabled || n == "" || seen[n] { continue } seen[n] = true out = append(out, n) } return out } // migrateScopeSvcConfig 服务实例。返回处理的全局行数。 func migrateScopeSvcConfig(first string, rest []string) (int, error) { rows := make([]*ThirdSvcConfig, 0) if err := postgres.Find(comm.TableSvcConfig, &rows, "app_name=?", ""); err != nil && err != postgres.ErrNoDocuments { return 0, err } if len(rows) == 0 { return 0, nil } for _, r := range rows { for _, app := range rest { exist := &ThirdSvcConfig{} if err := postgres.FindOne(comm.TableSvcConfig, exist, "app_name=? AND id=?", app, r.Id); err == nil { continue } cp := *r cp.RowId = 0 cp.AppName = app if err := postgres.Insert(comm.TableSvcConfig, &cp); err != nil { return 0, fmt.Errorf("拷贝 %s → %s: %w", r.Id, app, err) } } } // 拷完再改名(逐行):首个应用已有同 id 的(上次跑到一半留下的)就删掉全局行,否则改名。 for _, r := range rows { exist := &ThirdSvcConfig{} if postgres.FindOne(comm.TableSvcConfig, exist, "app_name=? AND id=?", first, r.Id) == nil { _ = postgres.Delete(comm.TableSvcConfig, "row_id=?", r.RowId) continue } if err := postgres.Table(comm.TableSvcConfig).Where("row_id=?", r.RowId).Update("app_name", first).Error; err != nil { return 0, err } } return len(rows), nil } func migrateScopeRegionOverride(first string, rest []string) (int, error) { rows := make([]*SvcRegionOverride, 0) if err := postgres.Find(comm.TableSvcRegionOverride, &rows, "app_name=?", ""); err != nil && err != postgres.ErrNoDocuments { return 0, err } if len(rows) == 0 { return 0, nil } for _, r := range rows { for _, app := range rest { exist := &SvcRegionOverride{} if err := postgres.FindOne(comm.TableSvcRegionOverride, exist, "app_name=? AND svc_id=? AND region=?", app, r.SvcId, r.Region); err == nil { continue } cp := *r cp.Id = 0 cp.AppName = app if err := postgres.Insert(comm.TableSvcRegionOverride, &cp); err != nil { return 0, fmt.Errorf("拷贝 %s@%d → %s: %w", r.SvcId, r.Region, app, err) } } } for _, r := range rows { exist := &SvcRegionOverride{} if postgres.FindOne(comm.TableSvcRegionOverride, exist, "app_name=? AND svc_id=? AND region=?", first, r.SvcId, r.Region) == nil { _ = postgres.Delete(comm.TableSvcRegionOverride, "id=?", r.Id) continue } if err := postgres.Table(comm.TableSvcRegionOverride).Where("id=?", r.Id).Update("app_name", first).Error; err != nil { return 0, err } } return len(rows), nil } func migrateScopeEchoOrch(first string, rest []string) (int, error) { rows := make([]*EchoOrchestration, 0) if err := postgres.Find(comm.TableEchomeetOrch, &rows, "app_name=?", ""); err != nil && err != postgres.ErrNoDocuments { return 0, err } if len(rows) == 0 { return 0, nil } for _, r := range rows { for _, app := range rest { exist := &EchoOrchestration{} if err := postgres.FindOne(comm.TableEchomeetOrch, exist, "app_name=? AND region=? AND kind=? AND svc_id=?", app, r.Region, r.Kind, r.SvcId); err == nil { continue } cp := *r cp.RowId = 0 cp.AppName = app if err := postgres.Insert(comm.TableEchomeetOrch, &cp); err != nil { return 0, err } } } for _, r := range rows { exist := &EchoOrchestration{} if postgres.FindOne(comm.TableEchomeetOrch, exist, "app_name=? AND region=? AND kind=? AND svc_id=?", first, r.Region, r.Kind, r.SvcId) == nil { _ = postgres.Delete(comm.TableEchomeetOrch, "row_id=?", r.RowId) continue } if err := postgres.Table(comm.TableEchomeetOrch).Where("row_id=?", r.RowId).Update("app_name", first).Error; err != nil { return 0, err } } return len(rows), nil } func migrateScopeEchoOrchSetting(first string, rest []string) (int, error) { rows := make([]*EchoOrchSetting, 0) if err := postgres.Find(comm.TableEchomeetOrchSetting, &rows, "app_name=?", ""); err != nil && err != postgres.ErrNoDocuments { return 0, err } if len(rows) == 0 { return 0, nil } for _, r := range rows { for _, app := range rest { exist := &EchoOrchSetting{} if err := postgres.FindOne(comm.TableEchomeetOrchSetting, exist, "app_name=? AND region=?", app, r.Region); err == nil { continue } cp := *r cp.RowId = 0 cp.AppName = app if err := postgres.Insert(comm.TableEchomeetOrchSetting, &cp); err != nil { return 0, err } } } for _, r := range rows { exist := &EchoOrchSetting{} if postgres.FindOne(comm.TableEchomeetOrchSetting, exist, "app_name=? AND region=?", first, r.Region) == nil { _ = postgres.Delete(comm.TableEchomeetOrchSetting, "row_id=?", r.RowId) continue } if err := postgres.Table(comm.TableEchomeetOrchSetting).Where("row_id=?", r.RowId).Update("app_name", first).Error; err != nil { return 0, err } } return len(rows), nil } // migrateScopeMeetTemplate 公共会议模板。表结构是 pb 生成的 struct(没有 AppName 字段), // app_name 列由 ensureConfigTables 用 ALTER 补上,这里走裸 SQL 按列名拷贝。 // 首个应用同样原行改名——客户端记录里存的模板 id 得保住。 func migrateScopeMeetTemplate(first string, rest []string) (int, error) { pg := postgres.GetSys() var n int64 if err := pg.Raw("SELECT COUNT(*) FROM " + comm.TableEchomeetTemplate + " WHERE source='public' AND COALESCE(app_name,'')=''").Scan(&n).Error; err != nil { return 0, err } if n == 0 { return 0, nil } cols := "tid, source, title, description, language, ttype, tags, icon, outline, template, sort" for _, app := range rest { // 已经有这个应用的同 tid+language 就跳过(NOT EXISTS),其余整批拷。 if err := pg.Exec("INSERT INTO "+comm.TableEchomeetTemplate+" ("+cols+", app_name) "+ "SELECT "+cols+", ? FROM "+comm.TableEchomeetTemplate+" g "+ "WHERE g.source='public' AND COALESCE(g.app_name,'')='' AND NOT EXISTS ("+ "SELECT 1 FROM "+comm.TableEchomeetTemplate+" x WHERE x.source='public' AND x.app_name=? AND x.tid=g.tid AND x.language=g.language)", app, app).Error; err != nil { return 0, err } } // 首个应用:已有同 tid+language 的删全局行,其余改名。 if err := pg.Exec("DELETE FROM "+comm.TableEchomeetTemplate+" g WHERE g.source='public' AND COALESCE(g.app_name,'')='' AND EXISTS ("+ "SELECT 1 FROM "+comm.TableEchomeetTemplate+" x WHERE x.source='public' AND x.app_name=? AND x.tid=g.tid AND x.language=g.language)", first).Error; err != nil { return 0, err } if err := pg.Exec("UPDATE "+comm.TableEchomeetTemplate+" SET app_name=? WHERE source='public' AND COALESCE(app_name,'')=''", first).Error; err != nil { return 0, err } return int(n), nil }