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>
This commit is contained in:
2025-11-18 16:44:06 +08:00
parent e8eb5766cb
commit eaa70ac255
86 changed files with 15395 additions and 245 deletions

View File

@@ -14,27 +14,75 @@ import (
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/handler"
"github.com/break/junhong_cmp_fiber/internal/middleware"
"github.com/break/junhong_cmp_fiber/internal/service/order"
"github.com/break/junhong_cmp_fiber/internal/service/user"
internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
"github.com/break/junhong_cmp_fiber/internal/routes"
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
permissionSvc "github.com/break/junhong_cmp_fiber/internal/service/permission"
roleSvc "github.com/break/junhong_cmp_fiber/internal/service/role"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/database"
"github.com/break/junhong_cmp_fiber/pkg/logger"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"github.com/break/junhong_cmp_fiber/pkg/validator"
)
func main() {
// 加载配置
// 1. 初始化配置
cfg := initConfig()
// 2. 初始化日志
appLogger := initLogger(cfg)
defer func() {
_ = logger.Sync()
}()
// 3. 初始化数据库
db := initDatabase(cfg, appLogger)
defer closeDatabase(db, appLogger)
// 4. 初始化 Redis
redisClient := initRedis(cfg, appLogger)
defer closeRedis(redisClient, appLogger)
// 5. 初始化队列客户端
queueClient := initQueue(redisClient, appLogger)
defer closeQueue(queueClient, appLogger)
// 6. 初始化 Services
services := initServices(db, redisClient, appLogger)
// 7. 启动配置监听器
watchCtx, cancelWatch := context.WithCancel(context.Background())
defer cancelWatch()
go config.Watch(watchCtx, appLogger)
// 8. 创建 Fiber 应用
app := createFiberApp(cfg, appLogger)
// 9. 注册中间件
initMiddleware(app, cfg, appLogger)
// 10. 注册路由
initRoutes(app, cfg, services, queueClient, db, redisClient, appLogger)
// 11. 启动服务器
startServer(app, cfg, appLogger, cancelWatch)
}
// initConfig 加载配置
func initConfig() *config.Config {
cfg, err := config.Load()
if err != nil {
panic("加载配置失败: " + err.Error())
}
return cfg
}
// 初始化日志
// initLogger 初始化日志
func initLogger(cfg *config.Config) *zap.Logger {
if err := logger.InitLoggers(
cfg.Logging.Level,
cfg.Logging.Development,
@@ -55,16 +103,34 @@ func main() {
); err != nil {
panic("初始化日志失败: " + err.Error())
}
defer func() {
_ = logger.Sync() // 忽略 sync 错误shutdown 时可能已经关闭)
}()
appLogger := logger.GetAppLogger()
appLogger.Info("应用程序启动中...",
zap.String("address", cfg.Server.Address),
)
appLogger.Info("应用程序启动中...", zap.String("address", cfg.Server.Address))
return appLogger
}
// initDatabase 初始化数据库连接
func initDatabase(cfg *config.Config, appLogger *zap.Logger) *gorm.DB {
db, err := database.InitPostgreSQL(&cfg.Database, appLogger)
if err != nil {
appLogger.Fatal("初始化 PostgreSQL 失败", zap.Error(err))
}
return db
}
// closeDatabase 关闭数据库连接
func closeDatabase(db *gorm.DB, appLogger *zap.Logger) {
sqlDB, _ := db.DB()
if sqlDB != nil {
if err := sqlDB.Close(); err != nil {
appLogger.Error("关闭 PostgreSQL 连接失败", zap.Error(err))
}
}
}
// initRedis 初始化 Redis 连接
func initRedis(cfg *config.Config, appLogger *zap.Logger) *redis.Client {
redisAddr := cfg.Redis.Address + ":" + strconv.Itoa(cfg.Redis.Port)
// 连接 Redis
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: cfg.Redis.Password,
@@ -75,64 +141,65 @@ func main() {
ReadTimeout: cfg.Redis.ReadTimeout,
WriteTimeout: cfg.Redis.WriteTimeout,
})
defer func() {
if err := redisClient.Close(); err != nil {
appLogger.Error("关闭 Redis 客户端失败", zap.Error(err))
}
}()
// 测试 Redis 连接
// 测试连接
ctx := context.Background()
if err := redisClient.Ping(ctx).Err(); err != nil {
appLogger.Fatal("连接 Redis 失败", zap.Error(err))
}
appLogger.Info("Redis 已连接", zap.String("address", redisAddr))
// 初始化 PostgreSQL 连接
db, err := database.InitPostgreSQL(&cfg.Database, appLogger)
if err != nil {
appLogger.Fatal("初始化 PostgreSQL 失败", zap.Error(err))
return redisClient
}
// closeRedis 关闭 Redis 连接
func closeRedis(redisClient *redis.Client, appLogger *zap.Logger) {
if err := redisClient.Close(); err != nil {
appLogger.Error("关闭 Redis 客户端失败", zap.Error(err))
}
defer func() {
sqlDB, _ := db.DB()
if sqlDB != nil {
if err := sqlDB.Close(); err != nil {
appLogger.Error("关闭 PostgreSQL 连接失败", zap.Error(err))
}
}
}()
}
// 初始化 Asynq 任务提交客户端
queueClient := queue.NewClient(redisClient, appLogger)
defer func() {
if err := queueClient.Close(); err != nil {
appLogger.Error("关闭 Asynq 客户端失败", zap.Error(err))
}
}()
// initQueue 初始化队列客户端
func initQueue(redisClient *redis.Client, appLogger *zap.Logger) *queue.Client {
return queue.NewClient(redisClient, appLogger)
}
// 创建令牌验证器
tokenValidator := validator.NewTokenValidator(redisClient, appLogger)
// closeQueue 关闭队列客户端
func closeQueue(queueClient *queue.Client, appLogger *zap.Logger) {
if err := queueClient.Close(); err != nil {
appLogger.Error("关闭 Asynq 客户端失败", zap.Error(err))
}
}
// 初始化 Store 层
store := postgres.NewStore(db, appLogger)
// initServices 初始化所有 Services
func initServices(db *gorm.DB, redisClient *redis.Client, appLogger *zap.Logger) *routes.Services {
// 初始化 RBAC Store 层
accountStore := postgres.NewAccountStore(db, redisClient)
roleStore := postgres.NewRoleStore(db)
permissionStore := postgres.NewPermissionStore(db)
accountRoleStore := postgres.NewAccountRoleStore(db)
rolePermissionStore := postgres.NewRolePermissionStore(db)
// 初始化 Service 层
userService := user.NewService(store, appLogger)
orderService := order.NewService(store, appLogger)
// 初始化 RBAC Service 层
accountService := accountSvc.New(accountStore, roleStore, accountRoleStore)
roleService := roleSvc.New(roleStore, permissionStore, rolePermissionStore)
permissionService := permissionSvc.New(permissionStore)
// 初始化 Handler 层
userHandler := handler.NewUserHandler(userService, appLogger)
orderHandler := handler.NewOrderHandler(orderService, appLogger)
taskHandler := handler.NewTaskHandler(queueClient, appLogger)
healthHandler := handler.NewHealthHandler(db, redisClient, appLogger)
accountHandler := handler.NewAccountHandler(accountService)
roleHandler := handler.NewRoleHandler(roleService)
permissionHandler := handler.NewPermissionHandler(permissionService)
// 启动配置文件监听器(热重载)
watchCtx, cancelWatch := context.WithCancel(context.Background())
defer cancelWatch()
go config.Watch(watchCtx, appLogger)
return &routes.Services{
AccountHandler: accountHandler,
RoleHandler: roleHandler,
PermissionHandler: permissionHandler,
}
}
// 创建 Fiber 应用
app := fiber.New(fiber.Config{
// createFiberApp 创建 Fiber 应用
func createFiberApp(cfg *config.Config, appLogger *zap.Logger) *fiber.App {
return fiber.New(fiber.Config{
AppName: "君鸿卡管系统 v1.0.0",
StrictRouting: true,
CaseSensitive: true,
@@ -141,12 +208,14 @@ func main() {
Prefork: cfg.Server.Prefork,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
ErrorHandler: middleware.ErrorHandler(appLogger), // 配置全局错误处理器
ErrorHandler: internalMiddleware.ErrorHandler(appLogger),
})
}
// 中间件注册(顺序很重要)
// initMiddleware 注册中间件
func initMiddleware(app *fiber.App, cfg *config.Config, appLogger *zap.Logger) {
// 1. Recover - 必须第一个,捕获所有 panic
app.Use(middleware.Recover(appLogger))
app.Use(internalMiddleware.Recover(appLogger))
// 2. RequestID - 为每个请求生成唯一 ID
app.Use(requestid.New(requestid.Config{
@@ -162,62 +231,54 @@ func main() {
app.Use(compress.New(compress.Config{
Level: compress.LevelDefault,
}))
}
// 路由注册
// initRoutes 注册路由
func initRoutes(app *fiber.App, cfg *config.Config, services *routes.Services, queueClient *queue.Client, db *gorm.DB, redisClient *redis.Client, appLogger *zap.Logger) {
// 注册模块化路由
routes.RegisterRoutes(app, services)
// 公共端点(无需认证
app.Get("/health", healthHandler.Check)
// API v1 路由组
// API v1 路由组(用于受保护的端点
v1 := app.Group("/api/v1")
// 受保护的端点(需要认证)
// 可选:启用认证中间件
if cfg.Middleware.EnableAuth {
v1.Use(middleware.KeyAuth(tokenValidator, appLogger))
// TODO: 配置 TokenValidator
appLogger.Info("认证中间件已启用")
}
// 可选:启用限流器
if cfg.Middleware.EnableRateLimiter {
var rateLimitStorage fiber.Storage
initRateLimiter(v1, cfg, appLogger)
}
}
// 根据配置选择存储后端
if cfg.Middleware.RateLimiter.Storage == "redis" {
rateLimitStorage = middleware.NewRedisStorage(
cfg.Redis.Address,
cfg.Redis.Password,
cfg.Redis.DB,
cfg.Redis.Port,
)
appLogger.Info("限流器使用 Redis 存储", zap.String("redis_address", cfg.Redis.Address))
} else {
rateLimitStorage = nil // 使用内存存储
appLogger.Info("限流器使用内存存储")
}
// initRateLimiter 初始化限流器
func initRateLimiter(router fiber.Router, cfg *config.Config, appLogger *zap.Logger) {
var rateLimitStorage fiber.Storage
v1.Use(middleware.RateLimiter(
cfg.Middleware.RateLimiter.Max,
cfg.Middleware.RateLimiter.Expiration,
rateLimitStorage,
))
if cfg.Middleware.RateLimiter.Storage == "redis" {
rateLimitStorage = internalMiddleware.NewRedisStorage(
cfg.Redis.Address,
cfg.Redis.Password,
cfg.Redis.DB,
cfg.Redis.Port,
)
appLogger.Info("限流器使用 Redis 存储", zap.String("redis_address", cfg.Redis.Address))
} else {
rateLimitStorage = nil
appLogger.Info("限流器使用内存存储")
}
// 用户路由
v1.Post("/users", userHandler.CreateUser)
v1.Get("/users/:id", userHandler.GetUser)
v1.Put("/users/:id", userHandler.UpdateUser)
v1.Delete("/users/:id", userHandler.DeleteUser)
v1.Get("/users", userHandler.ListUsers)
// 订单路由
v1.Post("/orders", orderHandler.CreateOrder)
v1.Get("/orders/:id", orderHandler.GetOrder)
v1.Put("/orders/:id", orderHandler.UpdateOrder)
v1.Get("/orders", orderHandler.ListOrders)
// 任务路由
v1.Post("/tasks/email", taskHandler.SubmitEmailTask)
v1.Post("/tasks/sync", taskHandler.SubmitSyncTask)
router.Use(internalMiddleware.RateLimiter(
cfg.Middleware.RateLimiter.Max,
cfg.Middleware.RateLimiter.Expiration,
rateLimitStorage,
))
}
// startServer 启动服务器
func startServer(app *fiber.App, cfg *config.Config, appLogger *zap.Logger, cancelWatch context.CancelFunc) {
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)