package console import ( "sort" "strings" "yunyan/pb" "github.com/gin-gonic/gin" ) // 「产品 → 应用」归属,以及由它推导出的账号可见应用集合。 // // 背景:产品(DBProduct,硬件产品)与品牌商同在 console 主库,与「应用」(app_product/app_registry) // 原本没有关联,导致品牌商/渠道商账号登录后拿不到任何应用(appbinds 只从账号手工勾选的 apps 推导), // 用户查询页「无可用应用」。现在产品可绑定一到多个应用名(DBProduct.Appnames,CSV),于是: // // 品牌商账号可见应用 = 本品牌名下全部产品绑定的应用名并集 // 渠道商账号可见应用 = 其所属品牌商的同一集合(渠道商卖的就是该品牌的产品) // // 沿用全局的「非空才限制」语义:并集为空(产品都没绑应用,如存量数据)时不做限制, // 免得升级后老账号突然一个应用都看不到。 // normalizeAppNames 归一「应用名 CSV」:去空白、去重、保序。空串原样返回。 func normalizeAppNames(csv string) string { seen := make(map[string]bool, 4) out := make([]string, 0, 4) for _, n := range splitCSV(csv) { if seen[n] { continue } seen[n] = true out = append(out, n) } return strings.Join(out, ",") } // checkAppNames 校验应用名都在应用中心(app_product)里;返回错误说明(空串=通过)。 // 查不到应用表时放行(不能因为注册表读失败就挡住产品保存)。 func (this *serverComp) checkAppNames(csv string) string { names := splitCSV(csv) if len(names) == 0 { return "" } all, err := this.module.model.listProducts() if err != nil || len(all) == 0 { return "" } known := make(map[string]bool, len(all)) for _, p := range all { known[p.Name] = true } for _, n := range names { if !known[n] { return "应用不存在: " + n } } return "" } // brandAppNames 取某品牌商名下全部产品绑定的应用名并集(走产品缓存,避免每次查库)。 // 返回空切片 = 该品牌的产品都没绑应用,调用方按「不限制」处理。 func (this *serverComp) brandAppNames(brandId uint32) []string { products, err := this.module.deviceCache.GetProducts() if err != nil { return nil } seen := make(map[string]bool, 4) out := make([]string, 0, 4) for _, p := range products { if p.Brandid != brandId { continue } for _, n := range splitCSV(p.Appnames) { if !seen[n] { seen[n] = true out = append(out, n) } } } sort.Strings(out) return out } // accountAppNames 当前账号在「应用」维度的绑定名单(空=该维度不限制): // 品牌商/渠道商账号按名下产品的应用归属推导,其余角色沿用账号手工勾选的 apps。 func (this *serverComp) accountAppNames(c *gin.Context) []string { s := scopeOf(c) if s.unlimited { return nil } if s.brandId != 0 { return this.brandAppNames(s.brandId) } return s.apps } // getMyApps 返回当前账号可选的应用部署列表([{id,name,app_name,region}],只含已启用的)。 // 前端的应用下拉统一调它:超管/管理员拿到全部,代理/运营按账号绑定,品牌商/渠道商按产品绑定的应用。 // 与登录响应里的 appbinds 同源,但登录态不会随产品绑定变化而更新,故页面按需再拉一次。 func (this *serverComp) getMyApps(c *gin.Context) { writeOK(c, gin.H{"apps": this.visibleAppBinds(this.accountAppNames(c), scopeOf(c).unlimited)}) } // visibleAppBinds 把「应用名绑定」展开成已启用的部署行(一个应用的国内/海外各一行)。 // 绑定值可能是部署注册名(历史数据)或应用名(后台按应用勾选),两者都算命中。 // unlimited(超管/管理员)→ 全部已启用部署;否则严格按 names 过滤,names 为空即返回空列表 // (下拉只呈现「确实绑定了的应用」;接口层的越权拦截另见 requireAppScope,那里沿用「空=不限制」)。 func (this *serverComp) visibleAppBinds(names []string, unlimited bool) []gin.H { out := make([]gin.H, 0, 4) all, err := this.module.model.listApps() if err != nil { return out } want := make(map[string]bool, len(names)) for _, n := range names { want[n] = true } for _, a := range all { if !a.Enabled { continue } if !unlimited && !want[a.Name] && !(a.AppName != "" && want[a.AppName]) { continue } out = append(out, gin.H{ "id": a.Id, "name": a.Name, "region": a.Region, "app_name": a.AppName, }) } return out } // requireAppNameScope 校验「按应用名作用域」的写操作在当前账号权限内(agent 等按 app_name 隔离的配置用)。 // 与 requireAppScope(按 X-App-Id)同一套口径,只是这里的作用域键是应用名: // - 超管/管理员(unlimited) 或 账号没有应用维度绑定 → 放行(沿用全局「非空才限制」语义); // - 有绑定的账号只能写自己名下的应用,且不能写「全局默认」(app_name=”)—— // 那是所有应用的兜底,改一处影响所有人,只该由超管/管理员维护。 // // 只用于写操作:读(列表)仍允许看全局默认,否则应用级账号看不到自己继承的那份配置。 func (this *serverComp) requireAppNameScope(c *gin.Context, appName string) bool { if scopeOf(c).unlimited { return true } allowed := this.accountAppNames(c) if len(allowed) == 0 { return true } if strings.TrimSpace(appName) == "" { writeErr(c, pb.ErrorCode_InsufficientPermissions, "无权修改「全局默认」作用域,请先选择具体应用") return false } if !containsStr(allowed, appName) { writeErr(c, pb.ErrorCode_InsufficientPermissions, "无权访问该应用: "+appName) return false } return true }