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 func (c *BackendRoleController) GetAllRoles() { tid, ok := c.currentTenantID() if !ok { return } // 懒加载补齐当前租户的默认角色(租户管理员/部门负责人/员工) models.EnsureDefaultTenantRoles(tid) var rows []models.AdminRole _, err := models.Orm.QueryTable(new(models.AdminRole)). Filter("cid", 2). Filter("tenant_id", tid). OrderBy("-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", 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 } 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 } // 仅允许更新本租户名下的角色,防止越权改到其他租户 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 } // 仅允许删除本租户名下的角色 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() }