// Package services 中本文件负责「展示端(platforms)」维度的读写与过滤。 // // 背景:模块与菜单原先只有 views([1]平台端 / [2]租户端)一个维度, // 无法表达「这个子系统/功能只在 Web 端出现,还是只在小程序出现,还是两端都出现」。 // 这里新增 platforms 维度: // ["web"] —— 仅在 Web 端(租户管理后台)展示 // ["app"] —— 仅在移动端(uniapp APP / 微信小程序)展示 // ["web","app"] —— 两端都展示 // 空串 / NULL —— 视为全端展示(保证历史数据行为不变) package services import ( "encoding/json" "strings" "server/models" ) // ParsePlatforms 解析展示端配置(JSON 数组字符串)。 // 兼容非数组的旧值(如单个字符串),解析失败一律返回空切片。 func ParsePlatforms(raw *string) []string { if raw == nil || strings.TrimSpace(*raw) == "" { return nil } var arr []string if err := json.Unmarshal([]byte(*raw), &arr); err == nil { out := make([]string, 0, len(arr)) for _, s := range arr { s = strings.ToLower(strings.TrimSpace(s)) if s != "" { out = append(out, s) } } return out } // 非数组格式:按单个值处理 v := strings.ToLower(strings.TrimSpace(*raw)) if v == "" { return nil } return []string{v} } // HasPlatform 判断是否在指定端展示。 // 未配置(空)视为全端展示,保证历史数据与新建记录默认两端可见。 func HasPlatform(list []string, platform string) bool { if len(list) == 0 { return true } p := strings.ToLower(strings.TrimSpace(platform)) for _, s := range list { if strings.ToLower(strings.TrimSpace(s)) == p { return true } } return false } // NormalizePlatforms 规范化并去重展示端配置,返回待落库的 JSON 字符串。 // 空输入返回空串(表示全端),便于前端「未配置 = 全端」的语义保持一致。 func NormalizePlatforms(list []string) string { set := map[string]bool{} out := make([]string, 0, len(list)) for _, s := range list { v := strings.ToLower(strings.TrimSpace(s)) if v == "" || set[v] { continue } set[v] = true out = append(out, v) } if len(out) == 0 { return "" } if b, err := json.Marshal(out); err == nil { return string(b) } return "" } // ListAllModulesForScope 读取全部未删除模块(含停用与 type=2 系统设置)。 // 展示端继承判定必须用全量模块:系统设置类模块默认不启用, // 若只用启用模块,其菜单会落到"无归属"被误判为全端可见。 func ListAllModulesForScope() ([]models.SystemModules, error) { var rows []models.SystemModules _, err := models.Orm.QueryTable(new(models.SystemModules)). Filter("delete_time__isnull", true). OrderBy("sort", "id"). All(&rows) return rows, err } // FilterModulesByPlatform 按展示端过滤模块 func FilterModulesByPlatform(rows []models.SystemModules, platform string) []models.SystemModules { out := make([]models.SystemModules, 0, len(rows)) for _, m := range rows { if HasPlatform(ParsePlatforms(m.Platforms), platform) { out = append(out, m) } } return out } // FilterMenusByPlatform 按展示端过滤菜单(只看菜单自身的 platforms,不做模块继承) func FilterMenusByPlatform(rows []models.SystemMenu, platform string) []models.SystemMenu { out := make([]models.SystemMenu, 0, len(rows)) for _, m := range rows { if HasPlatform(ParsePlatforms(m.Platforms), platform) { out = append(out, m) } } return out } // SortedModulesForScope 把模块按 path 长度倒序,供 MenuVisibleOn 的归属判定复用。 func SortedModulesForScope(modules []models.SystemModules) []models.SystemModules { return sortModulesByPathDesc(modules) } // MenuVisibleOn 判断单条菜单是否在指定端展示。 // 判定顺序:菜单自身 platforms → 继承所属模块 platforms(按 path 前缀归属)→ 全端可见。 // 这样后台只需在模块上配一次「仅 Web / 仅小程序」,其下未单独配置的菜单自动跟随。 func MenuVisibleOn(m models.SystemMenu, sortedModules []models.SystemModules, allModules []models.SystemModules, platform string) bool { if own := ParsePlatforms(m.Platforms); len(own) > 0 { return HasPlatform(own, platform) } path := "" if m.Path != nil { path = strings.TrimSpace(*m.Path) } code, ok := ownerModuleCode(sortedModules, path) if !ok { return true // 不属于任何模块 → 全端 } for _, mod := range allModules { if NormalizeModuleCode(mod.Code) == NormalizeModuleCode(code) { return HasPlatform(ParsePlatforms(mod.Platforms), platform) } } return true } // FilterMenusByPlatformWithModules 按展示端过滤菜单,并支持继承所属模块的展示端。 // modules 应传入全部模块(含停用与 type=2 系统设置),否则系统设置类菜单会误判为全端。 func FilterMenusByPlatformWithModules(rows []models.SystemMenu, modules []models.SystemModules, platform string) []models.SystemMenu { sorted := sortModulesByPathDesc(modules) out := make([]models.SystemMenu, 0, len(rows)) for _, m := range rows { if MenuVisibleOn(m, sorted, modules, platform) { out = append(out, m) } } return out }