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.
 
 
 
 
 
 

181 lines
9.7 KiB

package console
import (
"yunyan/comm"
"yunyan/lego/core"
"yunyan/lego/core/cbase"
"yunyan/lego/sys/postgres"
)
// AppRegistry 应用注册项,存在 console 的公共库(postgres.GetSys(),即 supabase 单库)。
// 每个应用一行,描述「在哪、连哪个业务库、配置事件往哪发」。
// - ServiceDsn → 应用 MySQL 业务库(userdevice/product_stat),仅「重置设备码」清用户绑定时连。
// - DeviceDsn → 已弃用:设备库内容(厂家/产品/设备码)已集中到 console 主库(supabase),注册应用无需再填。
//
// 注:DeviceDsn / Redis* 字段为历史保留,已不参与建连。
type AppRegistry struct {
Id uint32 `gorm:"primaryKey;autoIncrement;column:id" json:"id"`
Name string `gorm:"column:name;size:64" json:"name"` // 显示名,如 "Voitrans(国内)"
Region string `gorm:"column:region;size:32" json:"region"` // 区域标记,如 cn/hw
BaseUrl string `gorm:"column:base_url;size:255" json:"base_url"` // 应用对外地址(前端跳转/回调)
DeviceDsn string `gorm:"column:device_dsn;size:512" json:"device_dsn"` // 已弃用:设备库改走 console 主库(supabase)
ServiceDsn string `gorm:"column:service_dsn;size:512" json:"service_dsn"` // 业务库 DSN → 重置设备码时清 userdevice/product_stat
RedisAddr string `gorm:"column:redis_addr;size:255" json:"redis_addr"` // Redis 地址,多地址逗号分隔
RedisPwd string `gorm:"column:redis_pwd;size:255" json:"redis_pwd"`
RedisDb int `gorm:"column:redis_db" json:"redis_db"`
RedisTls bool `gorm:"column:redis_tls" json:"redis_tls"`
KeyPrefix string `gorm:"column:key_prefix;size:64" json:"key_prefix"` // Redis key 应用前缀
NatsSubject string `gorm:"column:nats_subject;size:128" json:"nats_subject"` // 配置事件 subject,空则回退默认
NatsStream string `gorm:"column:nats_stream;size:64" json:"nats_stream"`
Enabled bool `gorm:"column:enabled" json:"enabled"` // 是否启用(停用的不在切换器展示)
Remark string `gorm:"column:remark;size:255" json:"remark"`
// AppName 归属的应用名(= app_product.name,空=未分组)。用应用名直接关联——名称唯一即应用ID,不再套整型外键。
AppName string `gorm:"column:app_name;size:64;index" json:"app_name"`
Status string `gorm:"column:status;size:16" json:"status"` // existing=已接入现有 / pending=待部署(选了目标环境尚未部署)
// EnvId 关联的部署环境(app_environment.id,0=未关联);下面三个是下一期远程部署的应用维度覆盖(本期只存不用)。
EnvId uint32 `gorm:"column:env_id;index" json:"env_id"` // 所属部署环境 → app_environment.id
DeployDir string `gorm:"column:deploy_dir;size:255" json:"deploy_dir"` // 覆盖 env.DeployDir(对应 APPS 行远端目录)
ImageName string `gorm:"column:image_name;size:128" json:"image_name"` // 覆盖 env.ImageName(共用镜像时留空)
ImageTag string `gorm:"column:image_tag;size:64" json:"image_tag"` // 覆盖 env.ImageTag
Createtime int64 `gorm:"column:createtime" json:"createtime"`
Updatetime int64 `gorm:"column:updatetime" json:"updatetime"`
}
func (AppRegistry) TableName() string { return comm.TableAppRegistry }
// modelComp 注册库 CRUD(console 的公共库,走全局 postgres.GetSys() —— 即 supabase 单库)。
type modelComp struct {
cbase.ModuleCompBase
module *Console
}
func (this *modelComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) {
this.ModuleCompBase.Init(service, module, comp, opt)
this.module = module.(*Console)
if err = postgres.CreateTable(comm.TableAppRegistry, &AppRegistry{}); err != nil {
this.module.Errorln(err)
}
// app_registry 新增的关联/部署列:CreateTable 对已存在表跳过 AutoMigrate,故显式幂等补列。
if err = this.ensureRegistryColumns(); err != nil {
this.module.Errorln(err)
}
// 应用/产品表 + 部署环境表(新表,首次 CreateTable 会正常建)。
if err = postgres.CreateTable(comm.TableAppProduct, &AppProduct{}); err != nil {
this.module.Errorln(err)
}
if err = postgres.CreateTable(comm.TableAppEnvironment, &AppEnvironment{}); err != nil {
this.module.Errorln(err)
}
// 应用×服务的基础设施/运行配置表(业务服务启动 seed、console 后台读写;新表 CreateTable 幂等)。
if err = postgres.CreateTable(comm.TableAppServiceConfig, &comm.AppServiceConfig{}); err != nil {
this.module.Errorln(err)
}
if err = this.ensureAccountTable(); err != nil {
this.module.Errorln(err)
}
// 设备管理域表(厂家/产品/设备码/公码/出货单)建在 console 主库(supabase),启动建一次。
// SkipEnsureTables 打开时跳过(如指向已建好表的生产库、只读查看场景)。
if !this.module.options.SkipEnsureTables {
if err = ensureDeviceTables(); err != nil {
this.module.Errorln(err)
}
}
// 服务配置域三张共享表(全局环境配置/MCP/会议模板)建在 console 主库。
// CreateTable 对已存在的表是幂等空操作(不 AutoMigrate、不改 schema),
// 故即便 SkipEnsureTables=true(生产只读 schema)也安全执行——这三张为新表,需确保存在。
if err = ensureConfigTables(); err != nil {
this.module.Errorln(err)
}
// 月结算单表建在 console 主库。同 ensureConfigTables 的理由(新表、CreateTable 对已存在表是
// 幂等空操作),即便 SkipEnsureTables=true 也要执行——否则结算页查询直接 42P01。
if err = ensureSettlementTable(); err != nil {
this.module.Errorln(err)
}
// 凭据归一:两张扁平 env 表(global_config / 业务库 config) → svc_config。
// 幂等,每次启动跑,跑完即空转;细节与取舍见 migrate_envtosvc.go。
// 必须排在 ensureConfigTables 之后(要 svc_config 表已存在)。
if apps, e := this.listApps(); e != nil {
this.module.Errorln(e)
} else {
migrateEnvToSvcConfig(this.module.options.EncryptKey, apps)
// 作用域归一:全局层分给各应用(2026-09-14,每个应用一套互不干涉)。
// 必须排在 migrateEnvToSvcConfig 之后:那一步把凭据写进的是全局层,这一步再把全局层分下去。
migrateGlobalScopeToApps(apps)
// 应用参数归一:业务库 config 表里客户端还在读的键 → svc_config(按应用),死键删除。
// 必须排在 migrateGlobalScopeToApps 之后:直接写 app_name=<应用> 的行,不再经过全局层。
migrateAppParamsToSvc(this.module.options.EncryptKey, apps)
// 只读巡检:作用域键(部署 .env / 注册表两列 / 库里实际存的)对不上时点名告警,见 scope_check.go。
checkScopeConsistency(apps)
}
// 内置服务类别补种(幂等,只补语义标记不动展示属性)。
seedSvcCategories()
return
}
// listApps 返回全部注册应用(按 id 升序)。
func (this *modelComp) listApps() (models []*AppRegistry, err error) {
models = make([]*AppRegistry, 0)
err = postgres.Find(comm.TableAppRegistry, &models, "")
return
}
// getApp 按 id 取注册项。
func (this *modelComp) getApp(id uint32) (model *AppRegistry, err error) {
model = &AppRegistry{}
err = postgres.FindOne(comm.TableAppRegistry, model, "id=?", id)
return
}
// getAppByName 按名称取注册项;埋点信封按应用名校验是否为已注册应用(落库前校验)。
func (this *modelComp) getAppByName(name string) (model *AppRegistry, err error) {
model = &AppRegistry{}
err = postgres.FindOne(comm.TableAppRegistry, model, "name=?", name)
return
}
// getAppByNameOrAppName 按「部署名 name」或「应用名 app_name」任一命中查注册项。
//
// 后台页面上的应用选择器给的是 app_name(EAIMAR / deepGlass),而 getAppByName 只查 name;
// 两列在现网虽然同值,但按 CLAUDE.md 的口径它们语义不同,这里两列都认,别依赖它们恰好相等。
func (this *modelComp) getAppByNameOrAppName(name string) (model *AppRegistry, err error) {
model = &AppRegistry{}
err = postgres.FindOne(comm.TableAppRegistry, model, "name=? OR app_name=?", name, name)
return
}
// addApp 新增注册项(Insert 后 model.Id 被回填)。
func (this *modelComp) addApp(model *AppRegistry) (err error) {
err = postgres.Insert(comm.TableAppRegistry, model)
return
}
// saveApp 更新注册项。
func (this *modelComp) saveApp(model *AppRegistry) (err error) {
err = postgres.Save(comm.TableAppRegistry, model)
return
}
// delApp 删除注册项。
func (this *modelComp) delApp(id uint32) (err error) {
err = postgres.Delete(comm.TableAppRegistry, "id=?", id)
return
}
// ensureRegistryColumns 给已存在的 app_registry 幂等补充「环境关联 + 部署覆盖」列。
// CreateTable 对已存在表跳过 AutoMigrate(见 lego/sys/postgres),故新列须显式 ALTER(IF NOT EXISTS 幂等)。
func (this *modelComp) ensureRegistryColumns() error {
stmts := []string{
"ALTER TABLE " + comm.TableAppRegistry + " ADD COLUMN IF NOT EXISTS app_name varchar(64) DEFAULT ''",
"ALTER TABLE " + comm.TableAppRegistry + " ADD COLUMN IF NOT EXISTS status varchar(16) DEFAULT 'existing'",
"ALTER TABLE " + comm.TableAppRegistry + " ADD COLUMN IF NOT EXISTS env_id bigint DEFAULT 0",
"ALTER TABLE " + comm.TableAppRegistry + " ADD COLUMN IF NOT EXISTS deploy_dir varchar(255) DEFAULT ''",
"ALTER TABLE " + comm.TableAppRegistry + " ADD COLUMN IF NOT EXISTS image_name varchar(128) DEFAULT ''",
"ALTER TABLE " + comm.TableAppRegistry + " ADD COLUMN IF NOT EXISTS image_tag varchar(64) DEFAULT ''",
}
for _, sql := range stmts {
if res := postgres.Exec(sql); res.Error != nil {
return res.Error
}
}
return nil
}