Files
yunzerwebsiteallinone/go/controllers/backend_role.go
T
2026-09-15 10:44:31 +08:00

395 lines
11 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"
"io"
"strconv"
"strings"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// BackendRoleController 租户端(backend)角色管理(yz_system_admin_role, cid=2)
// 角色按租户隔离:所有读写均以当前登录租户(JWT 中的 tenant_id)为边界,
// 租户仅能查看/管理自己名下的角色,无法越权访问其他租户数据。
type BackendRoleController struct {
beego.Controller
}
type backendRolePayload struct {
Name string `json:"name"`
Status *uint8 `json:"status"`
Rights interface{} `json:"rights"`
}
// parseSubmittedIDs 将前端提交的 rights 解析为菜单 ID 列表(兼容 JSON 数组 / 逗号分隔字符串)。
func parseSubmittedIDs(v interface{}) []uint64 {
ids := make([]uint64, 0)
switch t := v.(type) {
case []interface{}:
for _, item := range t {
switch n := item.(type) {
case float64:
ids = append(ids, uint64(n))
case json.Number:
if i, err := n.Int64(); err == nil {
ids = append(ids, uint64(i))
}
case string:
if i, err := strconv.ParseUint(strings.TrimSpace(n), 10, 64); err == nil {
ids = append(ids, i)
}
}
}
case []uint64:
ids = append(ids, t...)
case string:
var arr []uint64
if err := json.Unmarshal([]byte(t), &arr); err == nil {
ids = append(ids, arr...)
} else {
for _, part := range strings.Split(t, ",") {
if i, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64); err == nil {
ids = append(ids, i)
}
}
}
}
return ids
}
// loadAssignableTenantMenuIDs 返回租户端可分配菜单的 ID 集合:cid=2 且已启用且已显示。
func loadAssignableTenantMenuIDs() map[uint64]bool {
ids := make(map[uint64]bool)
var menus []models.SystemMenu
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
return ids
}
menus = filterMenusByView(menus, 2)
menus = filterAssignableMenus(menus)
for _, m := range menus {
ids[m.ID] = true
}
return ids
}
// sanitizeTenantRights 在服务端收敛租户角色权限:
// 仅保留租户端(cid=2)已启用且已显示的菜单 ID,剔除平台菜单、停用菜单、隐藏菜单。
// rights 为 nil 或空串表示全权限(与 filterMenusByRights 保持一致)。
func sanitizeTenantRights(v interface{}) *string {
if v == nil {
return nil
}
if s, ok := v.(string); ok && strings.TrimSpace(s) == "" {
return nil
}
submitted := parseSubmittedIDs(v)
empty := "[]"
if len(submitted) == 0 {
return &empty
}
allowed := loadAssignableTenantMenuIDs()
kept := make([]uint64, 0, len(submitted))
seen := make(map[uint64]bool, len(submitted))
for _, id := range submitted {
if id == 0 || seen[id] || !allowed[id] {
continue
}
seen[id] = true
kept = append(kept, id)
}
if len(kept) == 0 {
return &empty
}
b, _ := json.Marshal(kept)
s := string(b)
return &s
}
// currentTenantID 从 Bearer Token 解析当前登录租户 ID。
// 返回 (tid, true) 表示成功;失败时已写入 401/403 响应并返回 (0, false)。
func (c *BackendRoleController) currentTenantID() (uint64, bool) {
authHeader := c.Ctx.Request.Header.Get("Authorization")
if authHeader == "" {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未登录"}
_ = c.ServeJSON()
return 0, false
}
authParts := strings.SplitN(authHeader, " ", 2)
if len(authParts) != 2 || authParts[0] != "Bearer" {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "认证信息格式错误"}
_ = c.ServeJSON()
return 0, false
}
claims, err := jwtutil.ParseToken(authParts[1])
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "无效的token"}
_ = c.ServeJSON()
return 0, false
}
if claims.UserType != "backend" && claims.UserType != "app" {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "无权访问"}
_ = c.ServeJSON()
return 0, false
}
if claims.TenantId <= 0 {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "租户信息缺失"}
_ = c.ServeJSON()
return 0, false
}
return uint64(claims.TenantId), true
}
// GetAllRoles 获取当前租户可见的角色列表
// GET /backend/allRoles
// 返回「全局角色(cid=2, tenant_id=0,所有租户共用)」+「本租户自建角色」;
// 全局角色由平台端统一维护,租户端只读。
func (c *BackendRoleController) GetAllRoles() {
tid, ok := c.currentTenantID()
if !ok {
return
}
var rows []models.AdminRole
_, err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("cid", 2).
Filter("tenant_id__in", []uint64{0, tid}).
OrderBy("tenant_id", "is_custom", "id").
All(&rows)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": rows}
_ = c.ServeJSON()
}
// GetRoleByID 获取当前租户的角色详情
// GET /backend/roles/:id
func (c *BackendRoleController) GetRoleByID() {
tid, ok := c.currentTenantID()
if !ok {
return
}
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
if id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
_ = c.ServeJSON()
return
}
var role models.AdminRole
if err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
Filter("cid", 2).
Filter("tenant_id__in", []uint64{0, tid}).
One(&role); err != nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "角色不存在"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": role}
_ = c.ServeJSON()
}
// CreateRole 创建当前租户的角色
// POST /backend/roles
func (c *BackendRoleController) CreateRole() {
tid, ok := c.currentTenantID()
if !ok {
return
}
var p backendRolePayload
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if err := json.Unmarshal(raw, &p); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
p.Name = strings.TrimSpace(p.Name)
if p.Name == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "name 不能为空"}
_ = c.ServeJSON()
return
}
// 不能与本租户可见的角色重名(含平台默认的全局角色),避免混淆
dup, derr := models.Orm.QueryTable(new(models.AdminRole)).
Filter("cid", 2).
Filter("tenant_id__in", []uint64{0, tid}).
Filter("name", p.Name).
Count()
if derr != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败"}
_ = c.ServeJSON()
return
}
if dup > 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "已存在同名角色(含平台默认角色),请更换名称"}
_ = c.ServeJSON()
return
}
status := uint8(1)
if p.Status != nil {
status = *p.Status
}
rights := sanitizeTenantRights(p.Rights)
// 租户端创建的角色一律标记为“自定义角色”,与系统/平台预置角色区分,并按租户隔离
role := &models.AdminRole{
TenantID: tid,
Cid: 2,
IsCustom: 1,
Name: p.Name,
Status: status,
Rights: rights,
}
id, err := models.Orm.Insert(role)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": map[string]interface{}{"id": id}}
_ = c.ServeJSON()
}
// UpdateRole 更新当前租户的角色
// PUT /backend/roles/:id
func (c *BackendRoleController) UpdateRole() {
tid, ok := c.currentTenantID()
if !ok {
return
}
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
if id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
_ = c.ServeJSON()
return
}
var p backendRolePayload
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if err := json.Unmarshal(raw, &p); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
update := map[string]interface{}{}
if strings.TrimSpace(p.Name) != "" {
update["name"] = strings.TrimSpace(p.Name)
}
if p.Status != nil {
update["status"] = *p.Status
}
if p.Rights != nil {
update["rights"] = sanitizeTenantRights(p.Rights)
}
if len(update) == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无更新字段"}
_ = c.ServeJSON()
return
}
// 全局角色(tenant_id=0)由平台端统一维护,租户端只读
var target models.AdminRole
if e := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
Filter("cid", 2).
One(&target); e == nil && target.TenantID == 0 {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "全局角色由平台端统一维护,租户端不能修改"}
_ = c.ServeJSON()
return
}
// 改名时不能与可见角色重名(含平台默认的全局角色,排除自身)
if newName, ok := update["name"]; ok {
dup, derr := models.Orm.QueryTable(new(models.AdminRole)).
Filter("cid", 2).
Filter("tenant_id__in", []uint64{0, tid}).
Filter("name", newName).
Exclude("id", id).
Count()
if derr != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败"}
_ = c.ServeJSON()
return
}
if dup > 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "已存在同名角色(含平台默认角色),请更换名称"}
_ = c.ServeJSON()
return
}
}
// 仅允许更新本租户名下的角色,防止越权改到其他租户
cnt, err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
Filter("cid", 2).
Filter("tenant_id", tid).
Update(update)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败"}
_ = c.ServeJSON()
return
}
if cnt == 0 {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "角色不存在"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// DeleteRole 删除当前租户的角色
// DELETE /backend/roles/:id
func (c *BackendRoleController) DeleteRole() {
tid, ok := c.currentTenantID()
if !ok {
return
}
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
if id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
_ = c.ServeJSON()
return
}
// 全局角色(tenant_id=0)由平台端统一维护,租户端不能删除
var target models.AdminRole
if e := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
Filter("cid", 2).
One(&target); e == nil && target.TenantID == 0 {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "全局角色由平台端统一维护,租户端不能删除"}
_ = c.ServeJSON()
return
}
// 仅允许删除本租户名下的角色
cnt, err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
Filter("cid", 2).
Filter("tenant_id", tid).
Delete()
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败"}
_ = c.ServeJSON()
return
}
if cnt == 0 {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "角色不存在"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}