458 lines
16 KiB
Go
458 lines
16 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
"kefu-cloud/server/internal/middleware"
|
|
"kefu-cloud/server/internal/model"
|
|
)
|
|
|
|
type PermissionHandler struct{}
|
|
|
|
func NewPermissionHandler() *PermissionHandler { return &PermissionHandler{} }
|
|
|
|
func ensurePermissionData(tenantID uint) error {
|
|
if err := model.EnsurePermissions(); err != nil {
|
|
return err
|
|
}
|
|
return model.EnsureBuiltinRoles(tenantID)
|
|
}
|
|
|
|
// ListPermissions 获取所有系统权限码(全租户共享,供权限配置页使用)。
|
|
func (h *PermissionHandler) ListPermissions(c *gin.Context) {
|
|
if err := ensurePermissionData(middleware.GetTenantID(c)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "初始化权限数据失败"})
|
|
return
|
|
}
|
|
var permissions []model.Permission
|
|
if err := model.DB.Order("sort_order asc").Find(&permissions).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询失败"})
|
|
return
|
|
}
|
|
middleware.JSON(c, permissions)
|
|
}
|
|
|
|
// ListRoles 获取当前租户的角色列表(含成员数、权限数)。
|
|
func (h *PermissionHandler) ListRoles(c *gin.Context) {
|
|
tenantID := middleware.GetTenantID(c)
|
|
if err := ensurePermissionData(tenantID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "初始化权限数据失败"})
|
|
return
|
|
}
|
|
var roles []model.Role
|
|
if err := model.DB.Where("tenant_id = ?", tenantID).Order("id asc").Find(&roles).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询失败"})
|
|
return
|
|
}
|
|
|
|
type roleWithStats struct {
|
|
model.Role
|
|
MemberCount int64 `json:"member_count"`
|
|
PermCount int64 `json:"perm_count"`
|
|
}
|
|
result := make([]roleWithStats, len(roles))
|
|
for i, role := range roles {
|
|
result[i].Role = role
|
|
if err := model.DB.Model(&model.User{}).Where("tenant_id = ? AND role = ?", tenantID, role.Code).Count(&result[i].MemberCount).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "统计角色成员失败"})
|
|
return
|
|
}
|
|
if err := model.DB.Model(&model.RolePermission{}).Where("role_id = ?", role.ID).Count(&result[i].PermCount).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "统计角色权限失败"})
|
|
return
|
|
}
|
|
}
|
|
middleware.JSON(c, result)
|
|
}
|
|
|
|
type roleAccessSnapshot struct {
|
|
Permissions []string `json:"permissions"`
|
|
DataScopes map[string]string `json:"data_scopes"`
|
|
}
|
|
|
|
func loadRoleAccess(tx *gorm.DB, role model.Role) (roleAccessSnapshot, error) {
|
|
snapshot := roleAccessSnapshot{Permissions: []string{}, DataScopes: model.DefaultRoleDataScopes(role.Code)}
|
|
if err := tx.Table("role_permissions rp").
|
|
Joins("JOIN permissions p ON p.id = rp.permission_id").
|
|
Where("rp.role_id = ?", role.ID).
|
|
Order("p.sort_order asc").
|
|
Pluck("p.code", &snapshot.Permissions).Error; err != nil {
|
|
return snapshot, err
|
|
}
|
|
var scopes []model.RoleDataScope
|
|
if err := tx.Where("role_id = ?", role.ID).Find(&scopes).Error; err != nil {
|
|
return snapshot, err
|
|
}
|
|
for _, scope := range scopes {
|
|
snapshot.DataScopes[scope.Module] = scope.Scope
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
// GetRole 获取角色详情(含权限码与数据范围)。
|
|
func (h *PermissionHandler) GetRole(c *gin.Context) {
|
|
tenantID := middleware.GetTenantID(c)
|
|
var role model.Role
|
|
if err := model.DB.Where("id = ? AND tenant_id = ?", c.Param("id"), tenantID).First(&role).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "角色不存在"})
|
|
return
|
|
}
|
|
access, err := loadRoleAccess(model.DB, role)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询角色权限失败"})
|
|
return
|
|
}
|
|
var memberCount int64
|
|
if err := model.DB.Model(&model.User{}).Where("tenant_id = ? AND role = ?", tenantID, role.Code).Count(&memberCount).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "统计角色成员失败"})
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{
|
|
"role": role, "permissions": access.Permissions,
|
|
"data_scopes": access.DataScopes, "member_count": memberCount,
|
|
})
|
|
}
|
|
|
|
type saveRoleReq struct {
|
|
Name *string `json:"name"`
|
|
Desc *string `json:"desc"`
|
|
Permissions []string `json:"permissions"`
|
|
DataScopes map[string]string `json:"data_scopes"`
|
|
}
|
|
|
|
func validateRoleName(name string) (string, error) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" || len([]rune(name)) > 50 {
|
|
return "", errors.New("角色名称需 1-50 个字符")
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
func validateRoleAccess(tx *gorm.DB, permissions []string, dataScopes map[string]string) ([]model.Permission, error) {
|
|
uniqueCodes := make(map[string]struct{}, len(permissions))
|
|
for _, code := range permissions {
|
|
code = strings.TrimSpace(code)
|
|
if code != "" {
|
|
uniqueCodes[code] = struct{}{}
|
|
}
|
|
}
|
|
codes := make([]string, 0, len(uniqueCodes))
|
|
for code := range uniqueCodes {
|
|
codes = append(codes, code)
|
|
}
|
|
sort.Strings(codes)
|
|
var records []model.Permission
|
|
if len(codes) > 0 {
|
|
if err := tx.Where("code IN ?", codes).Order("sort_order asc").Find(&records).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
if len(records) != len(codes) {
|
|
return nil, errors.New("包含无效权限码")
|
|
}
|
|
}
|
|
|
|
selected := make(map[string]bool, len(records))
|
|
moduleViews := make(map[string][]string)
|
|
for _, permission := range records {
|
|
selected[permission.Code] = true
|
|
}
|
|
var allPermissions []model.Permission
|
|
if err := tx.Order("sort_order asc").Find(&allPermissions).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, permission := range allPermissions {
|
|
if permission.Category == "view" {
|
|
moduleViews[permission.Module] = append(moduleViews[permission.Module], permission.Code)
|
|
}
|
|
}
|
|
for _, permission := range records {
|
|
if permission.Category != "operate" || len(moduleViews[permission.Module]) == 0 {
|
|
continue
|
|
}
|
|
hasView := false
|
|
for _, viewCode := range moduleViews[permission.Module] {
|
|
hasView = hasView || selected[viewCode]
|
|
}
|
|
if !hasView {
|
|
return nil, errors.New(permission.Module + "的操作权限必须同时启用查看权限")
|
|
}
|
|
}
|
|
|
|
allowedModules := make(map[string]bool)
|
|
for _, module := range model.DataScopeModules() {
|
|
allowedModules[module] = true
|
|
}
|
|
for module, scope := range dataScopes {
|
|
if !allowedModules[module] || (scope != model.DataScopeAll && scope != model.DataScopeSelf) {
|
|
return nil, errors.New("包含无效数据范围")
|
|
}
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func saveRoleAccess(tx *gorm.DB, role model.Role, permissions []string, dataScopes map[string]string) error {
|
|
records, err := validateRoleAccess(tx, permissions, dataScopes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("role_id = ?", role.ID).Delete(&model.RolePermission{}).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, permission := range records {
|
|
if err := tx.Create(&model.RolePermission{RoleID: role.ID, PermissionID: permission.ID}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for module, scope := range dataScopes {
|
|
item := model.RoleDataScope{RoleID: role.ID, Module: module}
|
|
if err := tx.Where(item).Assign(model.RoleDataScope{Scope: scope}).FirstOrCreate(&item).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// UpdatedAt 同时作为前端权限缓存版本,关联表变更时也必须推进。
|
|
return tx.Model(&role).UpdateColumn("updated_at", time.Now()).Error
|
|
}
|
|
|
|
func newCustomRoleCode() (string, error) {
|
|
random := make([]byte, 8)
|
|
if _, err := rand.Read(random); err != nil {
|
|
return "", err
|
|
}
|
|
return "custom_" + hex.EncodeToString(random), nil
|
|
}
|
|
|
|
// CreateRole 创建自定义角色。
|
|
func (h *PermissionHandler) CreateRole(c *gin.Context) {
|
|
tenantID := middleware.GetTenantID(c)
|
|
var req saveRoleReq
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.Name == nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
|
return
|
|
}
|
|
name, err := validateRoleName(*req.Name)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
|
return
|
|
}
|
|
var duplicate int64
|
|
if err := model.DB.Model(&model.Role{}).Where("tenant_id = ? AND name = ?", tenantID, name).Count(&duplicate).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "检查角色名称失败"})
|
|
return
|
|
}
|
|
if duplicate > 0 {
|
|
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "角色名称已被占用"})
|
|
return
|
|
}
|
|
code, err := newCustomRoleCode()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "生成角色标识失败"})
|
|
return
|
|
}
|
|
desc := ""
|
|
if req.Desc != nil {
|
|
desc = strings.TrimSpace(*req.Desc)
|
|
}
|
|
role := model.Role{TenantID: tenantID, Name: name, Code: code, Type: "custom", Desc: desc}
|
|
dataScopes := req.DataScopes
|
|
if dataScopes == nil {
|
|
dataScopes = model.DefaultRoleDataScopes(role.Code)
|
|
}
|
|
if err := model.DB.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Create(&role).Error; err != nil {
|
|
return err
|
|
}
|
|
return saveRoleAccess(tx, role, req.Permissions, dataScopes)
|
|
}); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
|
return
|
|
}
|
|
middleware.InvalidatePermissionCache(tenantID, role.Code)
|
|
logPermissionChange(c, "create_role", role, roleAccessSnapshot{}, roleAccessSnapshot{Permissions: req.Permissions, DataScopes: dataScopes})
|
|
middleware.JSON(c, role)
|
|
}
|
|
|
|
// UpdateRole 更新自定义角色信息、权限与数据范围,角色标识保持不变。
|
|
func (h *PermissionHandler) UpdateRole(c *gin.Context) {
|
|
tenantID := middleware.GetTenantID(c)
|
|
var role model.Role
|
|
if err := model.DB.Where("id = ? AND tenant_id = ?", c.Param("id"), tenantID).First(&role).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "角色不存在"})
|
|
return
|
|
}
|
|
if role.Type == "builtin" {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "内置角色请使用专用配置接口"})
|
|
return
|
|
}
|
|
var req saveRoleReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
|
return
|
|
}
|
|
before, err := loadRoleAccess(model.DB, role)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "读取原权限失败"})
|
|
return
|
|
}
|
|
if req.Permissions == nil {
|
|
req.Permissions = before.Permissions
|
|
}
|
|
if req.DataScopes == nil {
|
|
req.DataScopes = before.DataScopes
|
|
}
|
|
updates := map[string]interface{}{}
|
|
if req.Name != nil {
|
|
name, err := validateRoleName(*req.Name)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
|
return
|
|
}
|
|
var duplicate int64
|
|
model.DB.Model(&model.Role{}).Where("tenant_id = ? AND name = ? AND id <> ?", tenantID, name, role.ID).Count(&duplicate)
|
|
if duplicate > 0 {
|
|
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "角色名称已被占用"})
|
|
return
|
|
}
|
|
updates["name"] = name
|
|
}
|
|
if req.Desc != nil {
|
|
updates["desc"] = strings.TrimSpace(*req.Desc)
|
|
}
|
|
if err := model.DB.Transaction(func(tx *gorm.DB) error {
|
|
if len(updates) > 0 {
|
|
if err := tx.Model(&role).Updates(updates).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return saveRoleAccess(tx, role, req.Permissions, req.DataScopes)
|
|
}); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
|
return
|
|
}
|
|
model.DB.First(&role, role.ID)
|
|
after, _ := loadRoleAccess(model.DB, role)
|
|
middleware.InvalidatePermissionCache(tenantID, role.Code)
|
|
logPermissionChange(c, "update_role", role, before, after)
|
|
middleware.JSON(c, role)
|
|
}
|
|
|
|
// UpdateBuiltinRole 更新内置角色权限与数据范围,不允许清空权限或修改标识。
|
|
func (h *PermissionHandler) UpdateBuiltinRole(c *gin.Context) {
|
|
tenantID := middleware.GetTenantID(c)
|
|
var role model.Role
|
|
if err := model.DB.Where("id = ? AND tenant_id = ?", c.Param("id"), tenantID).First(&role).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "角色不存在"})
|
|
return
|
|
}
|
|
if role.Type != "builtin" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "仅内置角色可使用此接口"})
|
|
return
|
|
}
|
|
var req saveRoleReq
|
|
if err := c.ShouldBindJSON(&req); err != nil || len(req.Permissions) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "内置角色至少需要一项权限"})
|
|
return
|
|
}
|
|
before, err := loadRoleAccess(model.DB, role)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "读取原权限失败"})
|
|
return
|
|
}
|
|
if req.DataScopes == nil {
|
|
req.DataScopes = before.DataScopes
|
|
}
|
|
if role.Code == "admin" {
|
|
required := map[string]bool{
|
|
"permission.view": false, "permission.create_role": false,
|
|
"permission.delete_role": false, "permission.assign_role": false,
|
|
}
|
|
for _, code := range req.Permissions {
|
|
if _, exists := required[code]; exists {
|
|
required[code] = true
|
|
}
|
|
}
|
|
for _, enabled := range required {
|
|
if !enabled {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "管理员必须保留全部权限控制权限"})
|
|
return
|
|
}
|
|
}
|
|
for _, module := range model.DataScopeModules() {
|
|
req.DataScopes[module] = model.DataScopeAll
|
|
}
|
|
}
|
|
if err := model.DB.Transaction(func(tx *gorm.DB) error {
|
|
if req.Desc != nil {
|
|
if err := tx.Model(&role).Update("desc", strings.TrimSpace(*req.Desc)).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return saveRoleAccess(tx, role, req.Permissions, req.DataScopes)
|
|
}); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
|
return
|
|
}
|
|
model.DB.First(&role, role.ID)
|
|
after, _ := loadRoleAccess(model.DB, role)
|
|
middleware.InvalidatePermissionCache(tenantID, role.Code)
|
|
logPermissionChange(c, "update_builtin_role", role, before, after)
|
|
middleware.JSON(c, role)
|
|
}
|
|
|
|
// DeleteRole 删除无成员绑定的自定义角色。
|
|
func (h *PermissionHandler) DeleteRole(c *gin.Context) {
|
|
tenantID := middleware.GetTenantID(c)
|
|
var role model.Role
|
|
if err := model.DB.Where("id = ? AND tenant_id = ?", c.Param("id"), tenantID).First(&role).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "角色不存在"})
|
|
return
|
|
}
|
|
if role.Type == "builtin" {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "内置角色不可删除"})
|
|
return
|
|
}
|
|
var memberCount int64
|
|
if err := model.DB.Model(&model.User{}).Where("tenant_id = ? AND role = ?", tenantID, role.Code).Count(&memberCount).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "检查角色成员失败"})
|
|
return
|
|
}
|
|
if memberCount > 0 {
|
|
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "请先为该角色下的成员更换角色", "member_count": memberCount})
|
|
return
|
|
}
|
|
before, _ := loadRoleAccess(model.DB, role)
|
|
if err := model.DB.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Where("role_id = ?", role.ID).Delete(&model.RolePermission{}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("role_id = ?", role.ID).Delete(&model.RoleDataScope{}).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Delete(&role).Error
|
|
}); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "删除失败"})
|
|
return
|
|
}
|
|
middleware.InvalidatePermissionCache(tenantID, role.Code)
|
|
logPermissionChange(c, "delete_role", role, before, roleAccessSnapshot{})
|
|
middleware.JSON(c, gin.H{"message": "已删除"})
|
|
}
|
|
|
|
func logPermissionChange(c *gin.Context, action string, role model.Role, before, after roleAccessSnapshot) {
|
|
detail, _ := json.Marshal(gin.H{
|
|
"role": role.Name, "code": role.Code, "before": before, "after": after,
|
|
})
|
|
model.DB.Create(&model.OperationLog{
|
|
OperatorID: middleware.GetUserID(c), Action: action, Detail: string(detail),
|
|
TargetType: "role", TargetID: &role.ID, IP: c.ClientIP(),
|
|
})
|
|
}
|