Files
huang 028cfaa7aa feat: 实现权限检查功能并添加Redis缓存优化
- 完成 CheckPermission 方法的完整实现(账号→角色→权限查询链)
- 实现 Redis 缓存机制,大幅提升权限查询性能(~12倍提升)
- 自动缓存失效:角色/权限变更时清除相关用户缓存
- 新增完整的单元测试和集成测试(10个测试用例全部通过)
- 添加权限检查使用文档和缓存机制说明
- 归档 implement-permission-check OpenSpec 提案

性能优化:
- 首次查询: ~18ms(3次DB查询 + 1次Redis写入)
- 缓存命中: ~1.5ms(1次Redis查询)
- TTL: 30分钟,自动失效机制保证数据一致性
2026-01-16 18:15:32 +08:00

7.5 KiB
Raw Permalink Blame History

Technical Design: 权限检查服务实现

Context

当前权限系统已实现:

  • RBAC 数据模型Account - Role - Permission 多对多关联)
  • Store 层AccountRoleStore, RolePermissionStore, PermissionStore
  • 权限中间件框架(pkg/middleware/permission.go
  • 权限检查核心逻辑缺失CheckPermission 方法仅为占位)

目标:补全权限检查服务,使权限中间件能够正常工作。

约束

  • 严格遵循项目四层架构Handler → Service → Store → Model
  • 不引入外部依赖或框架
  • 保持 Go 惯用模式,避免过度抽象

Goals / Non-Goals

Goals

  1. 实现完整的权限检查逻辑(账号 → 角色 → 权限链式查询)
  2. 支持 platform 参数过滤all/web/h5
  3. 超级管理员自动通过所有权限检查
  4. 提供单元测试和集成测试覆盖

Non-Goals

  1. 不实现基于资源的权限RBAC 仅支持功能权限)
  2. 不实现权限继承或权限组(保持简单)
  3. 不实现动态权限(权限在数据库中静态配置)

Decisions

Decision 1: 依赖注入方式

选择: 在 PermissionService 结构体中注入 AccountRoleStoreRolePermissionStore

理由:

  • 符合项目依赖注入模式(通过结构体字段注入)
  • 避免循环依赖Service 依赖 StoreStore 不依赖 Service
  • 便于单元测试(可 mock Store 层)

替代方案:

  • 方案 A: 在 PermissionStore 中添加聚合查询方法
    • 缺点:违反 Store 层单一职责原则
  • 方案 B: 使用全局变量或服务定位器
    • 缺点:违反项目依赖注入原则

Decision 2: 超级管理员权限处理

选择: 在 CheckPermission 方法开头检查用户类型,超级管理员直接返回 true

理由:

  • 性能优化:避免无意义的数据库查询
  • 业务语义:超级管理员拥有所有权限
  • 与现有数据权限逻辑一致(数据权限也跳过超级管理员)

实现:

func (s *Service) CheckPermission(ctx, userID, permCode, platform) (bool, error) {
    // 1. 检查用户类型(需要从 context 或通过 AccountStore 查询)
    userType := middleware.GetUserTypeFromContext(ctx)
    if userType == constants.UserTypeSuperAdmin {
        return true, nil
    }
    
    // 2. 常规权限检查流程
    // ...
}

Decision 3: Platform 匹配逻辑

选择: 权限的 platform 字段支持三种值:all(任意端),web(仅后台),h5(仅 H5

匹配规则:

// 权限匹配条件:
// 1. permCode 完全匹配
// 2. platform 匹配:
//    - permission.platform == "all" → 任意 platform 参数都匹配
//    - permission.platform == platform → 精确匹配

示例:

查询权限: CheckPermission(userID, "user:create", "web")

权限数据库:
- {permCode: "user:create", platform: "all"}   → ✅ 匹配
- {permCode: "user:create", platform: "web"}   → ✅ 匹配
- {permCode: "user:create", platform: "h5"}    → ❌ 不匹配

Decision 4: 缓存策略(可选实现)

选择: 第一阶段不实现缓存,保持简单

理由:

  • 权限查询频率不高(仅在请求进入时检查一次)
  • 权限数据量小(通常 < 100 条权限,< 10 个角色/用户)
  • PostgreSQL 查询性能足够3 次简单查询 < 10ms
  • 避免缓存失效复杂性(角色变更时需清除所有关联用户缓存)

未来优化方向:

  • 如果性能测试发现瓶颈,可添加 Redis 缓存
  • 缓存粒度:permission:user:{userID}:perms[]string (权限编码列表)
  • 过期时间5 分钟
  • 失效策略:角色分配/权限分配变更时,清除相关用户缓存

Implementation Details

核心算法流程

func (s *Service) CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error) {
    // 步骤 1: 检查超级管理员
    userType := middleware.GetUserTypeFromContext(ctx)
    if userType == constants.UserTypeSuperAdmin {
        return true, nil
    }
    
    // 步骤 2: 查询用户的角色 ID 列表
    roleIDs, err := s.accountRoleStore.GetRoleIDsByAccountID(ctx, userID)
    if err != nil {
        return false, fmt.Errorf("failed to get user roles: %w", err)
    }
    if len(roleIDs) == 0 {
        return false, nil // 用户无角色,无权限
    }
    
    // 步骤 3: 查询角色的权限 ID 列表(去重)
    permIDs, err := s.rolePermStore.GetPermIDsByRoleIDs(ctx, roleIDs)
    if err != nil {
        return false, fmt.Errorf("failed to get role permissions: %w", err)
    }
    if len(permIDs) == 0 {
        return false, nil // 角色无权限
    }
    
    // 步骤 4: 查询权限详情
    permissions, err := s.permissionStore.GetByIDs(ctx, permIDs)
    if err != nil {
        return false, fmt.Errorf("failed to get permissions: %w", err)
    }
    
    // 步骤 5: 遍历匹配 permCode 和 platform
    for _, perm := range permissions {
        if perm.PermCode == permCode {
            // platform 匹配规则
            if perm.Platform == constants.PlatformAll || perm.Platform == platform {
                return true, nil
            }
        }
    }
    
    return false, nil // 未找到匹配的权限
}

数据库查询分析

查询次数: 3 次

  1. AccountRoleStore.GetRoleIDsByAccountID() - 1 次查询
  2. RolePermissionStore.GetPermIDsByRoleIDs() - 1 次查询(带去重)
  3. PermissionStore.GetByIDs() - 1 次查询(批量)

预估性能:

  • 单次权限检查 < 10ms本地数据库
  • 单次权限检查 < 20ms远程数据库

优化空间:

  • 如果未来需要,可添加缓存层减少查询次数
  • 数据库索引已存在(account_id, role_id, perm_id

Risks / Trade-offs

Risk 1: 多次数据库查询影响性能

影响: 每次权限检查需要 3 次数据库查询

缓解:

  • 查询简单,使用索引,性能可接受
  • 权限检查仅在请求入口执行一次(不在业务逻辑中频繁调用)
  • 如果未来需要,可添加缓存层

监控:

  • 在日志中记录权限检查耗时
  • 生产环境监控 API 响应时间

Risk 2: 角色或权限变更后权限立即生效

现状: 不使用缓存,权限变更立即生效

影响:

  • 无缓存一致性问题
  • 性能稍低(可接受)

未来优化:

  • 如果添加缓存,需实现缓存失效机制

Migration Plan

部署步骤

  1. 代码部署:

    • 合并代码到主分支
    • 部署 API 服务
  2. 验证:

    • 运行集成测试
    • 手动测试权限中间件
  3. 激活权限中间件(可选):

    • 在需要权限控制的路由上添加 RequirePermission 中间件
    • 逐步启用,监控错误率

Rollback Plan

  • 无破坏性变更,可直接回滚代码
  • 权限中间件默认未启用,不影响现有功能

Open Questions

  1. 是否需要添加权限检查日志审计?

    • 当前设计:仅在错误时记录日志
    • 可选:记录所有权限检查结果(包括成功)用于安全审计
    • 决策: 暂不实现,避免日志量过大
  2. 是否需要支持权限继承?

    • 当前设计:权限扁平化,不支持继承
    • 可选:支持父级权限自动包含子级权限
    • 决策: 暂不实现,保持简单
  3. 是否需要支持权限否定(黑名单)?

    • 当前设计:仅支持白名单(有权限才能访问)
    • 可选:支持明确拒绝某些权限
    • 决策: 暂不实现,通过角色分配控制即可