Files
yunzerwebsiteallinone/go/controllers/backend_menu.go
T
2026-09-16 00:22:17 +08:00

581 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"encoding/json"
"server/models"
"server/pkg/jwtutil"
"server/services"
"strconv"
"strings"
beego "github.com/beego/beego/v2/server/web"
)
type BackendMenuController struct {
beego.Controller
}
type AdminMenuController = BackendMenuController
type menuPayload struct {
Pid *int64 `json:"pid"`
Title *string `json:"title"`
Path *string `json:"path"`
ComponentPath *string `json:"component_path"`
Icon *string `json:"icon"`
Sort *int64 `json:"sort"`
Status *int8 `json:"status"`
IsVisible *int8 `json:"is_visible"`
Views []int `json:"views"`
Type *int8 `json:"type"`
Permission *string `json:"permission"`
}
func parseViews(raw *string) []int {
if raw == nil || strings.TrimSpace(*raw) == "" {
return nil
}
var arr []int
if err := json.Unmarshal([]byte(*raw), &arr); err != nil {
return nil
}
return arr
}
func hasView(arr []int, v int) bool {
for _, n := range arr {
if n == v {
return true
}
}
return false
}
func filterMenusByView(menus []models.SystemMenu, v int) []models.SystemMenu {
out := make([]models.SystemMenu, 0, len(menus))
for _, m := range menus {
views := parseViews(m.Views)
if v == 1 && len(views) == 0 {
out = append(out, m)
continue
}
if hasView(views, v) {
out = append(out, m)
}
}
return out
}
func (c *BackendMenuController) GetMenu() {
var menus []models.SystemMenu
_, err := models.Orm.QueryTable(new(models.SystemMenu)).Filter("status", 1).All(&menus)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(filterMenusByView(menus, 1), 0)}
_ = c.ServeJSON()
}
func (c *BackendMenuController) GetBackendMenu() {
var menus []models.SystemMenu
_, err := models.Orm.QueryTable(new(models.SystemMenu)).Filter("status", 1).All(&menus)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
_ = c.ServeJSON()
return
}
backendMenus := filterMenusByView(menus, 2)
// 按当前登录用户角色过滤可见功能菜单:
// 角色 rights 为空(或未分配角色)→ 全权限,返回全部后端菜单;
// 否则仅返回角色允许访问的菜单(含其祖先节点,保证树结构完整)。
idStr := c.Ctx.Input.Param(":id")
uid, _ := strconv.ParseUint(idStr, 10, 64)
tid := uint64(0)
if uid > 0 {
var tu models.SystemTenantUser
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", uid).
Filter("delete_time__isnull", true).
One(&tu); e == nil {
tid = tu.Tid
if tu.GroupID > 0 {
var role models.AdminRole
if re := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", tu.GroupID).
Filter("cid", 2).
One(&role); re == nil {
backendMenus = filterMenusByRights(backendMenus, role.Rights)
}
}
}
}
// 套餐功能开通标记:未开通模块的菜单保留但标记 locked(前端展示锁定态与升级提示,不再 404)
backendMenus, lockedMenuIDs := filterMenusByTenantPackageLocked(c.Ctx.Request.Header.Get("Authorization"), backendMenus, tid, true)
menuTree := buildMenuTree(backendMenus, 0)
markTreeLocked(menuTree, lockedMenuIDs)
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": menuTree}
_ = c.ServeJSON()
}
// backendJWTTenantID 从 Authorization 的 Bearer Token 中解析租户ID;取不到时返回 0。
// 说明:backend 端登录时 token 里写入了租户ID(GenerateToken 的 tenantId),
// 用它做套餐功能过滤比依赖前端传参更可靠。
func backendJWTTenantID(auth string) uint64 {
auth = strings.TrimSpace(auth)
if auth == "" {
return 0
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return 0
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil || claims == nil || claims.TenantId <= 0 {
return 0
}
return uint64(claims.TenantId)
}
// filterMenusByTenantPackage 按租户套餐包含的功能模块过滤菜单(剔除未开通模块,用于角色菜单分配等场景)。
// tid 优先取 token 中的租户ID,其次用调用方传入的兜底值;两者都取不到时不处理(保持原行为)。
func filterMenusByTenantPackage(authHeader string, menus []models.SystemMenu, tid uint64) []models.SystemMenu {
filtered, _ := filterMenusByTenantPackageLocked(authHeader, menus, tid, false)
return filtered
}
// filterMenusByTenantPackageLocked 按租户套餐处理菜单。
// markLocked=false:剔除未开通模块的菜单(原行为);markLocked=true:保留菜单并返回锁定菜单ID集合(含后代)。
func filterMenusByTenantPackageLocked(authHeader string, menus []models.SystemMenu, tid uint64, markLocked bool) ([]models.SystemMenu, map[uint64]bool) {
if jwtTid := backendJWTTenantID(authHeader); jwtTid > 0 {
tid = jwtTid
}
if tid == 0 {
return menus, nil
}
allModules, err := services.ListEnabledModules()
if err != nil || len(allModules) == 0 {
return menus, nil
}
allowed := services.GetTenantModuleCodes(tid)
// 安全兜底:租户没有绑定套餐或套餐未配置任何功能模块时不做过滤,
// 避免因套餐数据缺失导致租户端菜单整体消失。
if len(allowed) == 0 {
return menus, nil
}
if markLocked {
return services.MarkMenusLockedByTenantModules(menus, allModules, allowed)
}
return services.FilterMenusByTenantModules(menus, allModules, allowed), nil
}
// markTreeLocked 将菜单树中位于锁定集合的节点标记为 locked
func markTreeLocked(nodes []*menuNode, locked map[uint64]bool) {
if len(nodes) == 0 || len(locked) == 0 {
return
}
for _, n := range nodes {
if locked[n.ID] {
n.Locked = true
}
if len(n.Children) > 0 {
markTreeLocked(n.Children, locked)
}
}
}
// parseRightsToSet 将角色 rights 解析为菜单 ID 集合(兼容 JSON 数组 / 逗号分隔)
func parseRightsToSet(raw string) map[uint64]bool {
set := map[uint64]bool{}
var arr []string
if err := json.Unmarshal([]byte(raw), &arr); err == nil {
for _, s := range arr {
s = strings.TrimSpace(s)
if s == "" {
continue
}
if n, e := strconv.ParseUint(s, 10, 64); e == nil {
set[n] = true
}
}
return set
}
for _, s := range strings.Split(raw, ",") {
s = strings.TrimSpace(s)
if s == "" {
continue
}
if n, e := strconv.ParseUint(s, 10, 64); e == nil {
set[n] = true
}
}
return set
}
// filterMenusByRights 按角色 rights(菜单 ID 集合)过滤菜单,并补全祖先节点,保证树结构完整。
// rights 为空(nil / 空串)→ 原样返回(全权限)。
func filterMenusByRights(menus []models.SystemMenu, rights *string) []models.SystemMenu {
if rights == nil || strings.TrimSpace(*rights) == "" {
return menus
}
allowed := parseRightsToSet(*rights)
if len(allowed) == 0 {
return menus
}
pidOf := make(map[uint64]int64, len(menus))
byID := make(map[uint64]models.SystemMenu, len(menus))
for _, m := range menus {
pidOf[m.ID] = m.Pid
byID[m.ID] = m
}
final := make(map[uint64]bool, len(allowed))
for id := range allowed {
cur := id
for {
if _, ok := byID[cur]; !ok {
break
}
if final[cur] {
break
}
final[cur] = true
p := pidOf[cur]
if p == 0 {
break
}
cur = uint64(p)
}
}
out := make([]models.SystemMenu, 0, len(final))
for id := range final {
out = append(out, byID[id])
}
return out
}
func (c *BackendMenuController) GetTenantList() {
var tid uint64
if jwtTid := c.Ctx.Input.GetData("tid"); jwtTid != nil {
tid = jwtTid.(uint64)
}
if tid == 0 {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未登录或非法请求"}
_ = c.ServeJSON()
return
}
var menus []models.SystemMenu
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).Filter("status", 1).All(&menus); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取失败:" + err.Error()}
_ = c.ServeJSON()
return
}
tree := buildMenuTree(filterMenusByView(menus, 2), 0)
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "获取成功",
"data": map[string]interface{}{"list": tree, "total": len(tree)},
}
_ = c.ServeJSON()
}
func (c *BackendMenuController) GetAllMenus() {
var menus []models.SystemMenu
cid, _ := c.GetInt("cid")
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
_ = c.ServeJSON()
return
}
if cid == 1 {
menus = filterMenusByView(menus, 1)
} else if cid == 2 {
menus = filterMenusByView(menus, 2)
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(menus, 0)}
_ = c.ServeJSON()
}
// filterAssignableMenus 仅保留「已启用且已显示」的菜单。
// 停用或隐藏的菜单不参与角色权限分配,直接在服务端屏蔽,不依赖前端过滤。
func filterAssignableMenus(menus []models.SystemMenu) []models.SystemMenu {
out := make([]models.SystemMenu, 0, len(menus))
for _, m := range menus {
if m.Status != 1 {
continue
}
// is_visible 为空视为显示
if m.IsVisible != nil && *m.IsVisible == 0 {
continue
}
out = append(out, m)
}
return out
}
// GetAssignableMenus 获取可分配给角色的菜单树
// 过滤规则(均在服务端完成):
// 1. cid:按菜单所属端过滤(1=平台端 2=租户端,不传则不过滤)
// 2. status=1:仅已启用的菜单
// 3. is_visible != 0:仅已显示的菜单(为空视为显示)
//
// 父级菜单被过滤时,其子菜单一并不可分配(菜单树按 pid 构建,父级缺失则整条分支不会返回)。
// GET /backend/assignableMenus?cid=2
func (c *BackendMenuController) GetAssignableMenus() {
var menus []models.SystemMenu
cid, _ := c.GetInt("cid")
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
_ = c.ServeJSON()
return
}
if cid == 1 || cid == 2 {
menus = filterMenusByView(menus, cid)
}
menus = filterAssignableMenus(menus)
// 租户端:仅可分配当前租户套餐已开通的功能模块下的菜单
if cid == 2 {
menus = filterMenusByTenantPackage(c.Ctx.Request.Header.Get("Authorization"), menus, 0)
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(menus, 0)}
_ = c.ServeJSON()
}
func (c *BackendMenuController) GetAllBackendMenus() {
var menus []models.SystemMenu
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(filterMenusByView(menus, 2), 0)}
_ = c.ServeJSON()
}
type menuNode struct {
ID uint64 `json:"id"`
Pid int64 `json:"pid"`
Title string `json:"title"`
Path string `json:"path,omitempty"`
ComponentPath string `json:"component_path,omitempty"`
Icon string `json:"icon,omitempty"`
Sort int64 `json:"sort"`
Status int8 `json:"status"`
IsVisible *int8 `json:"is_visible,omitempty"`
Views []int `json:"views,omitempty"`
Type int8 `json:"type"`
Permission string `json:"permission,omitempty"`
Locked bool `json:"locked,omitempty"`
Children []*menuNode `json:"children,omitempty"`
}
func buildMenuTree(menus []models.SystemMenu, pid int64) []*menuNode {
var tree []*menuNode
for _, m := range menus {
if m.Pid == pid {
node := &menuNode{
ID: m.ID,
Pid: m.Pid,
Title: m.Title,
Sort: m.Sort,
Status: m.Status,
IsVisible: m.IsVisible,
Views: parseViews(m.Views),
Type: m.Type,
}
if m.Path != nil {
node.Path = *m.Path
}
if m.ComponentPath != nil {
node.ComponentPath = *m.ComponentPath
}
if m.Icon != nil {
node.Icon = *m.Icon
}
if m.Permission != nil {
node.Permission = *m.Permission
}
if children := buildMenuTree(menus, int64(m.ID)); len(children) > 0 {
node.Children = children
}
tree = append(tree, node)
}
}
return tree
}
func (c *BackendMenuController) UpdateMenuStatus() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效菜单ID"}
_ = c.ServeJSON()
return
}
var body struct {
Status *int8 `json:"status"`
}
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &body); err != nil || body.Status == nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
if _, err = models.Orm.QueryTable(new(models.SystemMenu)).Filter("id", id).Update(map[string]interface{}{"status": *body.Status}); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "success": true}
_ = c.ServeJSON()
}
func (c *BackendMenuController) CreateMenu() {
payload, ok := c.parseMenuPayload(true)
if !ok {
return
}
var viewsStr string
views := payload.Views
if len(views) == 0 {
views = []int{1}
}
if b, err := json.Marshal(views); err == nil {
viewsStr = string(b)
}
menu := models.SystemMenu{
Pid: valueInt64(payload.Pid, 0),
Title: strings.TrimSpace(valueString(payload.Title, "")),
Sort: valueInt64(payload.Sort, 0),
Status: valueInt8(payload.Status, 1),
IsVisible: ptrInt8(valueInt8(payload.IsVisible, 1)),
Views: &viewsStr,
Type: valueInt8(payload.Type, 1),
Path: ptrString(valueString(payload.Path, "")),
ComponentPath: ptrString(valueString(payload.ComponentPath, "")),
Icon: ptrString(valueString(payload.Icon, "")),
Permission: ptrString(valueString(payload.Permission, "")),
}
id, err := models.Orm.Insert(&menu)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
_ = c.ServeJSON()
}
func (c *BackendMenuController) UpdateMenu() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效菜单ID"}
_ = c.ServeJSON()
return
}
payload, ok := c.parseMenuPayload(false)
if !ok {
return
}
views := payload.Views
if len(views) == 0 {
views = []int{1}
}
viewsBytes, _ := json.Marshal(views)
update := map[string]interface{}{
"pid": valueInt64(payload.Pid, 0),
"title": strings.TrimSpace(valueString(payload.Title, "")),
"path": valueString(payload.Path, ""),
"component_path": valueString(payload.ComponentPath, ""),
"icon": valueString(payload.Icon, ""),
"sort": valueInt64(payload.Sort, 0),
"status": valueInt8(payload.Status, 1),
"is_visible": valueInt8(payload.IsVisible, 1),
"views": string(viewsBytes),
"type": valueInt8(payload.Type, 1),
"permission": valueString(payload.Permission, ""),
}
if _, err = models.Orm.QueryTable(new(models.SystemMenu)).Filter("id", id).Update(update); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
_ = c.ServeJSON()
}
func (c *BackendMenuController) DeleteMenu() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效菜单ID"}
_ = c.ServeJSON()
return
}
if _, err = models.Orm.QueryTable(new(models.SystemMenu)).Filter("id", id).Delete(); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功", "success": true}
_ = c.ServeJSON()
}
func (c *BackendMenuController) parseMenuPayload(needTitle bool) (*menuPayload, bool) {
var payload menuPayload
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return nil, false
}
if needTitle && strings.TrimSpace(valueString(payload.Title, "")) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "菜单名称不能为空"}
_ = c.ServeJSON()
return nil, false
}
return &payload, true
}
func valueString(v *string, def string) string {
if v == nil {
return def
}
return *v
}
func valueInt8(v *int8, def int8) int8 {
if v == nil {
return def
}
return *v
}
func valueInt64(v *int64, def int64) int64 {
if v == nil {
return def
}
return *v
}
func ptrString(v string) *string { return &v }
func ptrInt8(v int8) *int8 { return &v }