Files
junhong_cmp_fiber/cmd/api/main.go
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

308 lines
8.9 KiB
Go

package main
import (
"context"
"os"
"os/signal"
"strconv"
"syscall"
"github.com/bytedance/sonic"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/requestid"
"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"
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"
)
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,
logger.LogRotationConfig{
Filename: cfg.Logging.AppLog.Filename,
MaxSize: cfg.Logging.AppLog.MaxSize,
MaxBackups: cfg.Logging.AppLog.MaxBackups,
MaxAge: cfg.Logging.AppLog.MaxAge,
Compress: cfg.Logging.AppLog.Compress,
},
logger.LogRotationConfig{
Filename: cfg.Logging.AccessLog.Filename,
MaxSize: cfg.Logging.AccessLog.MaxSize,
MaxBackups: cfg.Logging.AccessLog.MaxBackups,
MaxAge: cfg.Logging.AccessLog.MaxAge,
Compress: cfg.Logging.AccessLog.Compress,
},
); err != nil {
panic("初始化日志失败: " + err.Error())
}
appLogger := logger.GetAppLogger()
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)
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
PoolSize: cfg.Redis.PoolSize,
MinIdleConns: cfg.Redis.MinIdleConns,
DialTimeout: cfg.Redis.DialTimeout,
ReadTimeout: cfg.Redis.ReadTimeout,
WriteTimeout: cfg.Redis.WriteTimeout,
})
// 测试连接
ctx := context.Background()
if err := redisClient.Ping(ctx).Err(); err != nil {
appLogger.Fatal("连接 Redis 失败", zap.Error(err))
}
appLogger.Info("Redis 已连接", zap.String("address", redisAddr))
return redisClient
}
// closeRedis 关闭 Redis 连接
func closeRedis(redisClient *redis.Client, appLogger *zap.Logger) {
if err := redisClient.Close(); err != nil {
appLogger.Error("关闭 Redis 客户端失败", zap.Error(err))
}
}
// initQueue 初始化队列客户端
func initQueue(redisClient *redis.Client, appLogger *zap.Logger) *queue.Client {
return queue.NewClient(redisClient, appLogger)
}
// closeQueue 关闭队列客户端
func closeQueue(queueClient *queue.Client, appLogger *zap.Logger) {
if err := queueClient.Close(); err != nil {
appLogger.Error("关闭 Asynq 客户端失败", zap.Error(err))
}
}
// 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)
// 初始化 RBAC Service 层
accountService := accountSvc.New(accountStore, roleStore, accountRoleStore)
roleService := roleSvc.New(roleStore, permissionStore, rolePermissionStore)
permissionService := permissionSvc.New(permissionStore)
// 初始化 Handler 层
accountHandler := handler.NewAccountHandler(accountService)
roleHandler := handler.NewRoleHandler(roleService)
permissionHandler := handler.NewPermissionHandler(permissionService)
return &routes.Services{
AccountHandler: accountHandler,
RoleHandler: roleHandler,
PermissionHandler: permissionHandler,
}
}
// 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,
JSONEncoder: sonic.Marshal,
JSONDecoder: sonic.Unmarshal,
Prefork: cfg.Server.Prefork,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
ErrorHandler: internalMiddleware.ErrorHandler(appLogger),
})
}
// initMiddleware 注册中间件
func initMiddleware(app *fiber.App, cfg *config.Config, appLogger *zap.Logger) {
// 1. Recover - 必须第一个,捕获所有 panic
app.Use(internalMiddleware.Recover(appLogger))
// 2. RequestID - 为每个请求生成唯一 ID
app.Use(requestid.New(requestid.Config{
Generator: func() string {
return uuid.NewString()
},
}))
// 3. Logger - 记录所有请求
app.Use(logger.Middleware())
// 4. Compress - 响应压缩
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)
// API v1 路由组(用于受保护的端点)
v1 := app.Group("/api/v1")
// 可选:启用认证中间件
if cfg.Middleware.EnableAuth {
// TODO: 配置 TokenValidator
appLogger.Info("认证中间件已启用")
}
// 可选:启用限流器
if cfg.Middleware.EnableRateLimiter {
initRateLimiter(v1, cfg, appLogger)
}
}
// initRateLimiter 初始化限流器
func initRateLimiter(router fiber.Router, cfg *config.Config, appLogger *zap.Logger) {
var rateLimitStorage fiber.Storage
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("限流器使用内存存储")
}
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)
go func() {
if err := app.Listen(cfg.Server.Address); err != nil {
appLogger.Fatal("服务器启动失败", zap.Error(err))
}
}()
appLogger.Info("服务器已启动", zap.String("address", cfg.Server.Address))
// 等待关闭信号
<-quit
appLogger.Info("正在关闭服务器...")
// 取消配置监听器
cancelWatch()
// 关闭 HTTP 服务器
if err := app.ShutdownWithTimeout(cfg.Server.ShutdownTimeout); err != nil {
appLogger.Error("强制关闭服务器", zap.Error(err))
}
appLogger.Info("服务器已停止")
}