package middleware import ( "net/http" "strconv" "sync" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "kefu-cloud/server/internal/model" ) type cacheEntry struct { db *gorm.DB perms map[string]bool dataScopes map[string]string version string expireAt time.Time } var ( permCache = sync.Map{} cacheTTL = 5 * time.Minute permCtxKey = "permissions" ) // LoadPermissions 加载当前用户角色的权限码集合,存入 context。 // 使用内存缓存减少数据库查询。 func LoadPermissions(c Context, _ uint, tenantID uint, role string) string { key := permissionCacheKey(tenantID, role) if raw, ok := permCache.Load(key); ok { if entry, ok := raw.(cacheEntry); ok && entry.db == model.DB && time.Now().Before(entry.expireAt) { c.Set(permCtxKey, entry.perms) c.Set(dataScopeCtxKey, entry.dataScopes) return entry.version } } var roleRecord model.Role err := model.DB.Where("tenant_id = ? AND code = ?", tenantID, role).First(&roleRecord).Error if err != nil { perms := legacyRolePermissions(role) dataScopes := model.DefaultRoleDataScopes(role) entry := cacheEntry{db: model.DB, perms: perms, dataScopes: dataScopes, version: "legacy:" + role, expireAt: time.Now().Add(cacheTTL)} permCache.Store(key, entry) c.Set(permCtxKey, perms) c.Set(dataScopeCtxKey, dataScopes) return entry.version } var codes []string model.DB.Table("role_permissions rp"). Joins("JOIN permissions p ON p.id = rp.permission_id"). Where("rp.role_id = ?", roleRecord.ID). Pluck("p.code", &codes) perms := make(map[string]bool, len(codes)) for _, c := range codes { perms[c] = true } dataScopes := model.DefaultRoleDataScopes(role) var scopes []model.RoleDataScope if err := model.DB.Where("role_id = ?", roleRecord.ID).Find(&scopes).Error; err == nil { for _, scope := range scopes { dataScopes[scope.Module] = scope.Scope } } entry := cacheEntry{ db: model.DB, perms: perms, dataScopes: dataScopes, version: roleRecord.UpdatedAt.UTC().Format(time.RFC3339Nano), expireAt: time.Now().Add(cacheTTL), } permCache.Store(key, entry) c.Set(permCtxKey, perms) c.Set(dataScopeCtxKey, dataScopes) return entry.version } // InvalidatePermissionCache 权限变更后清除角色缓存。 func InvalidatePermissionCache(tenantID uint, role string) { permCache.Delete(permissionCacheKey(tenantID, role)) } // InvalidateTenantPermissionCache 清除租户下所有角色缓存。 func InvalidateTenantPermissionCache(tenantID uint) { prefix := strconv.FormatUint(uint64(tenantID), 10) + ":" permCache.Range(func(k, _ interface{}) bool { if key, ok := k.(string); ok { if len(key) > len(prefix) && key[:len(prefix)] == prefix { permCache.Delete(key) } } return true }) } const dataScopeCtxKey = "data_scopes" func permissionCacheKey(tenantID uint, role string) string { return strconv.FormatUint(uint64(tenantID), 10) + ":" + role } // GetDataScope 返回当前角色在指定模块的数据范围,异常或未知范围按 self 收紧。 func GetDataScope(c Context, module string) string { raw, ok := c.Get(dataScopeCtxKey) if !ok { return model.DataScopeSelf } scopes, ok := raw.(map[string]string) if !ok || scopes[module] != model.DataScopeAll { return model.DataScopeSelf } return model.DataScopeAll } // CanAccessAllData 判断当前请求是否可读取指定模块的全租户数据。 func CanAccessAllData(c Context, module string) bool { return GetDataScope(c, module) == model.DataScopeAll } // RequirePermission 在路由层强制校验任意一个权限码。 func RequirePermission(codes ...string) gin.HandlerFunc { return func(c *gin.Context) { if HasAnyPermission(c, codes...) { c.Next() return } model.DB.Create(&model.OperationLog{ OperatorID: GetUserID(c), Action: "permission_denied", Detail: "接口权限不足: " + c.Request.Method + " " + c.FullPath(), TargetType: "permission", IP: c.ClientIP(), }) c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权限执行此操作"}) c.Abort() } } // AuditDataAccessDenied 记录通过参数尝试访问越权数据的行为。 func AuditDataAccessDenied(c *gin.Context, targetType string, targetID *uint) { model.DB.Create(&model.OperationLog{ OperatorID: GetUserID(c), Action: "data_access_denied", Detail: "数据范围越权: " + c.Request.Method + " " + c.FullPath(), TargetType: targetType, TargetID: targetID, IP: c.ClientIP(), }) } // HasPermission 检查当前用户是否拥有指定权限码。 func HasPermission(c Context, code string) bool { raw, ok := c.Get(permCtxKey) if !ok { return false } perms, ok := raw.(map[string]bool) if !ok { return false } return perms[code] } // HasAnyPermission 检查是否拥有任意一项权限码。 func HasAnyPermission(c Context, codes ...string) bool { raw, ok := c.Get(permCtxKey) if !ok { return false } perms, ok := raw.(map[string]bool) if !ok { return false } for _, code := range codes { if perms[code] { return true } } return false } // GetPermissions 返回当前用户所有权限码集合。 func GetPermissions(c Context) map[string]bool { raw, ok := c.Get(permCtxKey) if !ok { return nil } perms, ok := raw.(map[string]bool) if !ok { return nil } return perms } // legacyRolePermissions 当角色表尚无记录时的兜底权限映射。 func legacyRolePermissions(role string) map[string]bool { base := map[string]map[string]bool{ "platform_admin": {}, "admin": { "session.view": true, "session.reply": true, "session.transfer": true, "session.end": true, "session.note": true, "session.priority": true, "customer.view": true, "customer.create": true, "customer.edit": true, "customer.export": true, "customer.tag": true, "chat_history.view": true, "chat_history.detail": true, "chat_history.export": true, "chat_history.batch_archive": true, "knowledge.view": true, "knowledge.create": true, "knowledge.edit": true, "knowledge.publish": true, "knowledge.delete": true, "quick_reply.view": true, "quick_reply.team_create": true, "quick_reply.team_edit": true, "quick_reply.personal_manage": true, "blacklist.view": true, "blacklist.create": true, "blacklist.delete": true, "statistics.view": true, "statistics.export": true, "statistics.performance": true, "settings.basic": true, "settings.channel": true, "settings.staff": true, "settings.assign_rule": true, "settings.customer_tag": true, "settings.auto_reply": true, "settings.worktime": true, "settings.notification": true, "permission.view": true, "permission.create_role": true, "permission.delete_role": true, "permission.assign_role": true, }, "supervisor": { "session.view": true, "session.reply": true, "session.transfer": true, "session.end": true, "session.note": true, "session.priority": true, "customer.view": true, "customer.create": true, "customer.edit": true, "customer.export": true, "customer.tag": true, "chat_history.view": true, "chat_history.detail": true, "chat_history.export": true, "chat_history.batch_archive": true, "knowledge.view": true, "knowledge.create": true, "knowledge.edit": true, "knowledge.publish": true, "knowledge.delete": true, "quick_reply.view": true, "quick_reply.team_create": true, "quick_reply.team_edit": true, "quick_reply.personal_manage": true, "blacklist.view": true, "blacklist.create": true, "blacklist.delete": true, "statistics.view": true, "statistics.export": true, "statistics.performance": true, "settings.customer_tag": true, "settings.auto_reply": true, }, "agent": { "session.view": true, "session.reply": true, "session.end": true, "session.note": true, "session.priority": true, "customer.view": true, "customer.create": true, "customer.edit": true, "customer.tag": true, "chat_history.view": true, "chat_history.detail": true, "knowledge.view": true, "quick_reply.view": true, "quick_reply.personal_manage": true, "statistics.view": true, "statistics.performance": true, }, } if perms, ok := base[role]; ok { return perms } return nil } type Context interface { Get(key string) (value interface{}, exists bool) Set(key string, value interface{}) }