主要功能: - 实现完整的 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>
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_id和shop_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:
- Given 数据库迁移脚本已准备, When 执行数据库迁移, Then 系统成功创建5个表(accounts、roles、permissions、account_roles、role_permissions),每个表包含所有必需字段
- Given 表已创建, When 检查表结构, Then 所有表包含标准字段(id、created_at、updated_at、deleted_at、creator、updater、status)
- Given 账号表已创建, When 检查表结构, Then 包含用户名、手机号、密码、用户类型、店铺ID、上级ID等字段
- Given 权限表已创建, When 检查表结构, Then 支持层级关系(parent_id字段)和排序(sort字段)
- Given GORM模型已定义, When 使用GORM创建测试数据, Then 数据成功插入,created_at和updated_at自动填充
- Given GORM模型已定义, When 执行软删除操作, Then 记录的deleted_at字段被设置,查询时自动排除已删除记录
- 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:
- Given 用户A(ID=1,parent_id=null,user_type=root)登录, When 查询任意业务数据, Then 系统返回所有数据,不应用过滤条件
- Given 用户B(ID=2,parent_id=1,shop_id=10)登录, When 查询数据, Then 系统自动添加WHERE条件:owner_id IN (2, 及所有B的下级ID) AND shop_id = 10
- 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的数据
- Given 用户E(ID=5,parent_id=2,shop_id=20), When 用户B(ID=2,shop_id=10)查询数据, Then 系统不返回用户E创建的数据(尽管E是B的下级,但shop_id不同)
- Given Store层方法接收context参数, When context中包含当前用户ID和shop_id, Then GORM自动从context提取用户ID和shop_id并应用数据权限过滤
- Given 某些特殊查询需要跳过过滤, When 调用Store方法时传入WithoutDataFilter选项, Then 系统不应用数据权限过滤
- 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:
- Given main函数过长(200+行), When 重构为多个初始化函数, Then main函数代码行数减少至100行以内,只负责编排
- Given 初始化逻辑已拆分, When 查看代码结构, Then 存在独立函数:initConfig、initLogger、initDatabase、initRedis、initQueue、initServices、initMiddleware、initRoutes
- Given 路由直接写在main函数中, When 按模块拆分路由, Then 创建文件:internal/routes/routes.go(总入口)、internal/routes/user.go、internal/routes/order.go、internal/routes/health.go、internal/routes/task.go
- Given 路由已模块化, When main函数调用routes.RegisterRoutes(app, handlers), Then 该函数内部调用各模块的路由注册函数
- 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 fmt、go vet、golangci-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关联关系标签(
foreignKey、references、hasMany、belongsTo等) - 表关联通过存储关联ID字段手动维护
- 关联数据查询在代码层显式执行,不依赖ORM自动加载或预加载
- 模型结构体只包含简单字段,不包含其他模型的嵌套引用
- 数据库迁移脚本禁止外键约束定义
- 数据库迁移脚本禁止触发器维护关联数据
- 时间字段(
created_at、updated_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