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(), }) }