Files
junhong_cmp_fiber/specs/004-rbac-data-permission/spec.md
huang eaa70ac255 feat: 实现 RBAC 权限系统和数据权限控制 (004-rbac-data-permission)
主要功能:
- 实现完整的 RBAC 权限系统(账号、角色、权限的多对多关联)
- 基于 owner_id + shop_id 的自动数据权限过滤
- 使用 PostgreSQL WITH RECURSIVE 查询下级账号
- Redis 缓存优化下级账号查询性能(30分钟过期)
- 支持多租户数据隔离和层级权限管理

技术实现:
- 新增 Account、Role、Permission 模型及关联关系表
- 实现 GORM Scopes 自动应用数据权限过滤
- 添加数据库迁移脚本(000002_rbac_data_permission、000003_add_owner_id_shop_id)
- 完善错误码定义(1010-1027 为 RBAC 相关错误)
- 重构 main.go 采用函数拆分提高可读性

测试覆盖:
- 添加 Account、Role、Permission 的集成测试
- 添加数据权限过滤的单元测试和集成测试
- 添加下级账号查询和缓存的单元测试
- 添加 API 回归测试确保向后兼容

文档更新:
- 更新 README.md 添加 RBAC 功能说明
- 更新 CLAUDE.md 添加技术栈和开发原则
- 添加 docs/004-rbac-data-permission/ 功能总结和使用指南

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:44:06 +08:00

20 KiB

Feature Specification: RBAC表结构与GORM数据权限过滤

Feature Branch: 004-rbac-data-permission
Created: 2025-11-17
Status: Draft
Input: 用户描述: "添加RBAC表结构、实现GORM租户系统(数据权限过滤)、主函数重构及路由优化"

Clarifications

Session 2025-11-17

  • Q: 您提到creator不代表归属,未来会有"分配/分销"功能。请问数据归属和权限过滤应该如何设计? → A: 在业务表添加owner_id字段,数据权限过滤改为仅基于owner_id(忽略creator)
  • Q: 如果用户的上下级关系形成循环(如A→B→C→A),递归查询下级ID时会陷入死循环。请问如何处理? → A: 不会出现循环,系统设计为:只有本级能建下级账号,parent_id在账号创建时设置且不可更改
  • Q: 如果某些查询(如公开API)没有登录用户,context中没有用户ID,数据权限过滤应该如何处理? → A: 系统有两种用户:B端账号用户(accounts表,基于owner_id过滤)和C端业务用户(通过C端认证中间件识别,特定分组路由,只能查看特定业务数据,需跳过owner_id过滤使用业务字段过滤)
  • Q: 如果用户层级关系有10层或更多,每次查询都递归查询所有下级ID可能影响性能。请问是否需要缓存这个下级ID列表? → A: 需要缓存到Redis,设置30分钟过期时间,账号关系变更时主动清除缓存
  • Q: 如果账号A被软删除(deleted_at不为NULL),归属于账号A的数据(owner_id=A)是否仍然对A的上级可见? → A: 软删除账号后,该账号的数据对上级仍然可见(递归查询下级ID包含已删除账号)
  • Q: 跨店铺数据访问控制策略 - 规格中账号表包含shop_id字段(店铺ID)和owner_id字段(数据归属者)。当用户查询业务数据时,数据权限过滤应该如何处理shop_id? → A: 同时使用owner_id和shop_id双重过滤(账号只能访问同店铺且归属于自己或下级的数据)
  • Q: 账号密码字段的安全处理 - 账号表的password字段存储MD5哈希值。在查询账号信息(如列表查询、详情查询)时,返回给客户端的数据是否应该包含密码字段? → A: 查询时排除密码字段(使用GORM标签json:"-"或DTO过滤,任何情况不返回)
  • Q: 关联表的软删除策略 - account_roles(账号-角色关联)和role_permissions(角色-权限关联)是否需要deleted_at字段支持软删除? → A: 需要软删除(account_roles和role_permissions都包含deleted_at字段,支持软删除和审计追踪)
  • Q: 数据分配时owner_id更新和历史记录 - 当数据从用户A分配给用户B时,系统应该如何处理owner_id字段的更新和历史追踪? → A: 直接更新owner_id,在独立的数据变更日志表(data_transfer_log)记录分配历史(包含原owner_id、新owner_id、操作人、操作时间、原因等)
  • Q: 高并发场景下的context隔离机制 - 在高并发场景下,每个请求的context中包含不同用户的user_idshop_id,系统如何确保这些context不会混淆? → A: 依赖Fiber框架的请求隔离(每个请求独立的goroutine和context,通过参数显式传递,无需额外机制)

User Scenarios & Testing (mandatory)

User Story 1 - 数据库表结构和GORM模型定义 (Priority: P1)

系统需要创建5个RBAC相关的数据库表(账号、角色、权限、账号-角色、角色-权限),并定义对应的GORM模型结构体,支持层级关系和软删除。

Why this priority: 这是整个权限系统的数据基础,没有表结构和模型,后续的租户系统和权限功能都无法实现。

Independent Test: 可以通过运行数据库迁移脚本、检查表结构、创建测试数据来独立验证表和模型是否正确定义。

Acceptance Scenarios:

  1. Given 数据库迁移脚本已准备, When 执行数据库迁移, Then 系统成功创建5个表(accounts、roles、permissions、account_roles、role_permissions),每个表包含所有必需字段
  2. Given 表已创建, When 检查表结构, Then 所有表包含标准字段(id、created_at、updated_at、deleted_at、creator、updater、status)
  3. Given 账号表已创建, When 检查表结构, Then 包含用户名、手机号、密码、用户类型、店铺ID、上级ID等字段
  4. Given 权限表已创建, When 检查表结构, Then 支持层级关系(parent_id字段)和排序(sort字段)
  5. Given GORM模型已定义, When 使用GORM创建测试数据, Then 数据成功插入,created_at和updated_at自动填充
  6. Given GORM模型已定义, When 执行软删除操作, Then 记录的deleted_at字段被设置,查询时自动排除已删除记录
  7. Given 关联表(account_roles、role_permissions)已创建, When 删除账号-角色或角色-权限关联, Then 系统执行软删除(设置deleted_at),保留审计历史

User Story 2 - GORM自动数据权限过滤(租户系统) (Priority: P1)

系统在GORM查询时自动应用数据权限过滤:根据当前登录用户的ID、层级关系和店铺归属,自动添加WHERE条件,使用户只能查询归属于自己和下级且在同一店铺的数据(基于owner_id和shop_id双重过滤,而非creator字段)。root账号不受限制。

Why this priority: 数据权限过滤是核心安全功能,确保数据隔离,防止越权访问,必须在P1阶段完成。

Independent Test: 可以通过创建层级用户数据、使用不同用户身份执行查询、验证返回结果是否正确过滤来独立测试。

Acceptance Scenarios:

  1. Given 用户A(ID=1,parent_id=null,user_type=root)登录, When 查询任意业务数据, Then 系统返回所有数据,不应用过滤条件
  2. Given 用户B(ID=2,parent_id=1,shop_id=10)登录, When 查询数据, Then 系统自动添加WHERE条件:owner_id IN (2, 及所有B的下级ID) AND shop_id = 10
  3. Given 用户C(ID=3,parent_id=2,shop_id=10)和用户D(ID=4,parent_id=2,shop_id=10), When 用户B(ID=2,shop_id=10)查询数据, Then 系统返回owner_id为2、3、4且shop_id为10的数据
  4. Given 用户E(ID=5,parent_id=2,shop_id=20), When 用户B(ID=2,shop_id=10)查询数据, Then 系统不返回用户E创建的数据(尽管E是B的下级,但shop_id不同)
  5. Given Store层方法接收context参数, When context中包含当前用户ID和shop_id, Then GORM自动从context提取用户ID和shop_id并应用数据权限过滤
  6. Given 某些特殊查询需要跳过过滤, When 调用Store方法时传入WithoutDataFilter选项, Then 系统不应用数据权限过滤
  7. Given 用户层级关系为A→B→C→D(4层), When 用户A查询数据, Then 系统正确递归查询所有下级ID(B、C、D)并结合shop_id应用过滤

User Story 3 - 主函数重构和路由模块化 (Priority: P2)

将main函数中的初始化逻辑拆分为独立的辅助函数,将路由注册按业务模块拆分到internal/routes/目录下的独立文件中。

Why this priority: 代码组织优化提升可维护性,但不影响功能交付,可以在核心功能完成后进行。

Independent Test: 可以通过运行应用、验证所有现有端点正常工作、检查代码结构来独立测试。

Acceptance Scenarios:

  1. Given main函数过长(200+行), When 重构为多个初始化函数, Then main函数代码行数减少至100行以内,只负责编排
  2. Given 初始化逻辑已拆分, When 查看代码结构, Then 存在独立函数:initConfig、initLogger、initDatabase、initRedis、initQueue、initServices、initMiddleware、initRoutes
  3. Given 路由直接写在main函数中, When 按模块拆分路由, Then 创建文件:internal/routes/routes.go(总入口)、internal/routes/user.go、internal/routes/order.go、internal/routes/health.go、internal/routes/task.go
  4. Given 路由已模块化, When main函数调用routes.RegisterRoutes(app, handlers), Then 该函数内部调用各模块的路由注册函数
  5. Given 代码重构完成, When 运行应用并测试所有现有API端点, Then 所有端点功能正常,无回归问题

Edge Cases

  • 用户上下级关系规则: 只有本级能建下级账号(A创建B,B创建C),parent_id在账号创建时设置且不可更改,因此不会出现循环关系
  • 软删除用户的数据权限: 账号被软删除后,该账号的数据(owner_id=该账号ID)对上级仍然可见,递归查询下级ID时包含已删除账号
  • 深层级性能优化: 用户的所有下级ID列表必须缓存到Redis(30分钟过期),账号关系变更时主动清除缓存,避免每次查询都递归查询
  • C端业务用户的数据权限: C端用户通过C端认证中间件识别(通常基于特定路由分组,如 /api/c/...),他们的数据权限过滤不使用owner_id,而是基于业务字段(如WHERE iccid = ?或WHERE device_id = ?),C端认证中间件在context中设置特定标记,触发Store层跳过owner_id过滤
  • creator字段用途: creator字段仅用于审计追踪(记录原始创建人),不参与数据权限过滤,对吗?

Requirements (mandatory)

Functional Requirements

  • FR-001: 系统必须创建账号表(accounts),包含字段:id、username、phone、password(MD5)、user_type(1=root,2=平台,3=代理,4=企业)、shop_id、parent_id、status(0=禁用,1=启用)、created_at、updated_at、creator、updater、deleted_at
  • FR-002: 系统必须创建角色表(roles),包含字段:id、role_name、role_desc、role_type(1=超级,2=代理,3=企业)、status、created_at、updated_at、creator、updater、deleted_at
  • FR-003: 系统必须创建权限表(permissions),包含字段:id、perm_name、perm_type(1=菜单,2=按钮)、url、parent_id、perm_code、sort、status、created_at、updated_at、creator、updater、deleted_at
  • FR-004: 系统必须创建账号-角色关联表(account_roles),包含字段:id、account_id、role_id、status、created_at、updated_at、creator、updater、deleted_at
  • FR-005: 系统必须创建角色-权限关联表(role_permissions),包含字段:id、role_id、perm_id、status、created_at、updated_at、creator、updater、deleted_at
  • FR-006: 系统必须为每个表定义对应的GORM模型结构体(Account、Role、Permission、AccountRole、RolePermission),放置在internal/model/目录
  • FR-006.1: 系统必须在Account模型的password字段上使用GORM标签json:"-",确保查询账号信息时不返回密码哈希值给客户端
  • FR-007: 系统必须在所有5个GORM模型(包括关联表AccountRole和RolePermission)中配置软删除支持(gorm.DeletedAt类型),支持审计追踪和撤销操作
  • FR-008: 系统必须禁止在GORM模型中使用关联关系标签(foreignKey、references、hasMany、belongsTo等),表关联通过ID字段手动维护
  • FR-009: 系统必须为所有业务表添加owner_id字段(INT类型,允许NULL)和shop_id字段(INT类型,允许NULL),分别表示数据归属者和店铺归属,用于数据权限过滤
  • FR-009.1: 系统必须实现数据权限过滤机制:在Store层查询时,自动根据context中的用户ID和shop_id添加WHERE条件:(owner_id IN (...) AND shop_id = ?)
  • FR-009.2: creator字段仅用于审计追踪(记录原始创建人),不参与数据权限过滤逻辑
  • FR-010: 系统必须支持递归查询用户的所有下级ID:给定用户ID,查询所有直接和间接下级的ID列表
  • FR-011: 系统必须对root账号(user_type=1)跳过数据权限过滤,允许查看所有数据
  • FR-012: 系统必须提供WithoutDataFilter选项,允许特定查询跳过基于owner_id和shop_id的数据权限过滤(用于C端业务用户场景,改为使用业务字段如iccid/device_id进行过滤)
  • FR-013: 系统必须通过context.Context在Handler→Service→Store之间传递当前用户ID和shop_id
  • FR-014: 系统必须将main函数拆分为多个初始化函数,每个函数负责一项初始化任务(配置、日志、数据库等)
  • FR-015: 系统必须将路由注册按业务模块拆分:创建internal/routes/包,包含routes.go(总入口)和各业务模块路由文件
  • FR-016: 账号的parent_id字段在创建时设置,创建后不可更改,确保上下级关系的不变性
  • FR-017: 只有本级账号能创建下级账号(例如A创建B,B创建C),禁止跨级创建(A不能直接创建C)
  • FR-018: 系统必须支持两种用户体系:B端账号用户(accounts表,使用owner_id和shop_id双重数据权限过滤)和C端业务用户(通过C端认证中间件识别,在context中设置SkipOwnerFilter标记,跳过owner_id和shop_id过滤,使用业务字段过滤)
  • FR-019: 系统必须将用户的所有下级ID列表缓存到Redis,key格式为account:subordinates:{账号ID},value为下级ID列表(JSON数组),过期时间30分钟
  • FR-020: 系统必须在账号的parent_id字段变更(虽然正常情况不可更改,但数据修复场景可能需要)或账号软删除时,主动清除相关的下级ID缓存
  • FR-021: 递归查询用户的所有下级ID时,必须包含已软删除的账号(deleted_at不为NULL的账号仍被视为下级),确保软删除账号的数据对上级仍然可见
  • FR-022: 系统在应用数据权限过滤时,必须同时验证owner_id和shop_id:只返回owner_id在用户的下级ID列表中且shop_id与当前用户一致的数据
  • FR-023 (🔮 未来功能): 系统将支持数据分配功能:当数据从用户A分配给用户B时,直接更新业务数据的owner_id为用户B的ID
  • FR-024 (🔮 未来功能): 系统将在数据分配时,在独立的数据变更日志表(data_transfer_log)记录分配历史,包含字段:id、table_name(业务表名)、record_id(业务数据ID)、old_owner_id(原归属者)、new_owner_id(新归属者)、operator_id(操作人)、transfer_reason(分配原因)、created_at
  • FR-025 (🔮 未来功能): 数据变更日志表(data_transfer_log)将支持查询:给定业务表和记录ID,可以查询完整的归属变更历史链
  • FR-026: 系统必须确保每个HTTP请求的context独立隔离:通过Fiber框架的请求级goroutine和显式参数传递,禁止使用全局变量存储用户信息,确保并发请求的用户身份不会混淆

Technical Requirements (Constitution-Driven)

Tech Stack Compliance:

  • 所有HTTP操作使用Fiber框架(禁止net/http快捷方式)
  • 所有数据库操作使用GORM(禁止database/sql直接调用)
  • 所有JSON操作使用sonic(禁止encoding/json)
  • 所有异步任务使用Asynq
  • 所有日志使用Zap + Lumberjack.v2
  • 所有配置使用Viper
  • 使用Go官方工具链:go fmtgo vetgolangci-lint

Architecture Requirements:

  • 实现遵循Handler → Service → Store → Model分层架构
  • 依赖通过结构体字段注入(不使用构造函数模式)
  • 统一错误码定义在pkg/errors/
  • 统一API响应通过pkg/response/
  • 所有常量定义在pkg/constants/(禁止magic numbers/strings)
  • 禁止硬编码值:3个以上相同字面量必须提取为常量
  • 已定义的常量必须使用(禁止重复硬编码)
  • 代码注释使用中文(实现注释用中文)
  • 日志消息使用中文(logger.Info/Warn/Error/Debug用中文)
  • 错误消息支持中文(用户可见错误有中文文本)
  • 所有Redis key通过pkg/constants/的key生成函数管理
  • 包结构扁平化,按功能组织(不按层次)

Go Idiomatic Design Requirements:

  • 禁止Java风格模式:禁止getter/setter方法、禁止I-前缀接口、禁止Impl-后缀
  • 接口小而专注(1-3个方法),在使用方定义
  • 错误处理显式(返回错误,不用panic)
  • 使用组合(结构体嵌入)不用继承
  • 并发使用goroutines和channels
  • 命名遵循Go规范:UserID不是userId,HTTPServer不是HttpServer
  • 禁止匈牙利命名法或类型前缀
  • 代码简单直接

API Design Requirements:

  • 所有API遵循RESTful原则
  • 所有响应使用统一JSON格式(code/message/data/timestamp)
  • 所有错误消息包含错误码和双语描述
  • 所有分页使用标准参数(page、page_size、total)
  • 所有时间字段使用ISO 8601格式(RFC3339)
  • 所有货币金额使用整数(分)

Performance Requirements:

  • API响应时间: P95 < 200ms, P99 < 500ms
  • 数据库查询: P95 < 50ms, P99 < 100ms
  • 递归查询下级ID: P95 < 50ms, P99 < 100ms (含Redis缓存)
  • 批量操作使用批量查询
  • 列表查询实现分页(默认20,最大100)
  • 非实时操作委托给异步任务
  • 使用context.Context进行超时和取消控制

Error Handling Requirements:

  • 所有API错误使用统一JSON格式(通过pkg/errors/全局ErrorHandler)
  • Handler层返回错误(禁止手动c.Status().JSON()处理错误)
  • 业务错误使用pkg/errors.New()pkg/errors.Wrap()并指定错误码
  • 所有错误码定义在pkg/errors/codes.go
  • 所有panic被Recover中间件捕获,转换为500响应
  • 错误日志包含完整请求上下文(Request ID、路径、方法、参数)
  • 5xx服务端错误自动脱敏(通用消息给客户端,完整错误在日志)
  • 4xx客户端错误可返回具体业务消息
  • 业务代码禁止panic(除非不可恢复的编程错误)
  • 错误码分类:0=成功,1xxx=客户端(4xx),2xxx=服务端(5xx)

Testing Requirements:

  • Service层业务逻辑有单元测试
  • 所有API端点有集成测试
  • 测试使用Go标准testing框架,*_test.go文件
  • 多测试用例使用table-driven tests
  • 测试独立运行,使用mocks/testcontainers
  • 目标覆盖率:70%+整体,90%+核心业务逻辑

Database Design Requirements (Constitution Principle):

  • 禁止表之间建立外键约束(Foreign Key Constraints)
  • GORM模型禁止使用ORM关联关系标签(foreignKeyreferenceshasManybelongsTo等)
  • 表关联通过存储关联ID字段手动维护
  • 关联数据查询在代码层显式执行,不依赖ORM自动加载或预加载
  • 模型结构体只包含简单字段,不包含其他模型的嵌套引用
  • 数据库迁移脚本禁止外键约束定义
  • 数据库迁移脚本禁止触发器维护关联数据
  • 时间字段(created_atupdated_at)由GORM自动处理,不使用数据库触发器

Key Entities

  • Account(账号): 代表系统用户账号,包含身份信息(用户名、手机号、MD5密码)、类型(1=root,2=平台,3=代理,4=企业)、层级关系(上级ID)、绑定关系(店铺ID)、状态、创建人、更新人、时间戳
  • Role(角色): 代表权限角色,包含角色名称、角色描述、角色类型(1=超级,2=代理,3=企业)、状态、创建人、更新人、时间戳
  • Permission(权限): 代表系统功能权限,包含权限名称、权限类型(1=菜单,2=按钮)、URL路径、层级关系(上级ID)、权限编码、排序、状态、创建人、更新人、时间戳
  • AccountRole(账号-角色关联): 代表用户与角色的多对多关系,包含账号ID、角色ID、状态、创建人、更新人、时间戳
  • RolePermission(角色-权限关联): 代表角色与权限的多对多关系,包含角色ID、权限ID、状态、创建人、更新人、时间戳

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: 数据库迁移脚本执行成功,5个表全部创建,包含所有必需字段,无外键约束
  • SC-002: GORM模型定义完整,可以成功创建、查询、更新、软删除数据,created_at和updated_at自动填充
  • SC-003: 数据权限过滤在3层用户层级下,查询响应时间增加不超过10ms(P95)
  • SC-004: root账号(user_type=1)可以查询100%的数据,普通用户只能查询自己和下级创建且在同一店铺的数据,数据隔离准确率100%
  • SC-005: main函数代码行数减少至100行以内,初始化逻辑拆分为至少6个独立函数
  • SC-006: 路由按模块拆分后,每个路由文件代码行数不超过100行,职责单一
  • SC-007: 代码重构后,运行所有现有集成测试,通过率100%,无回归问题
  • SC-008: 数据权限过滤支持至少5层用户层级(A→B→C→D→E),递归查询下级ID性能: P95 < 50ms, P99 < 100ms