Files
2026-10-03 23:46:55 +08:00

155 lines
5.2 KiB
Go

// 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
}