You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

263 lines
9.8 KiB

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
}