553 lines
17 KiB
Go
553 lines
17 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 套餐功能开通过滤:仅展示租户套餐已包含的功能模块对应的菜单(未包含的整块菜单不展示)
|
||
backendMenus = filterMenusByTenantPackage(c.Ctx.Request.Header.Get("Authorization"), backendMenus, tid)
|
||
|
||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(backendMenus, 0)}
|
||
_ = 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 {
|
||
if jwtTid := backendJWTTenantID(authHeader); jwtTid > 0 {
|
||
tid = jwtTid
|
||
}
|
||
if tid == 0 {
|
||
return menus
|
||
}
|
||
allModules, err := services.ListEnabledModules()
|
||
if err != nil || len(allModules) == 0 {
|
||
return menus
|
||
}
|
||
allowed := services.GetTenantModuleCodes(tid)
|
||
// 安全兜底:租户没有绑定套餐或套餐未配置任何功能模块时不做过滤,
|
||
// 避免因套餐数据缺失导致租户端菜单整体消失。
|
||
if len(allowed) == 0 {
|
||
return menus
|
||
}
|
||
return services.FilterMenusByTenantModules(menus, allModules, allowed)
|
||
}
|
||
|
||
// 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"`
|
||
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 }
|