Files
junhong_cmp_fiber/pkg/config/config.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

180 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"errors"
"fmt"
"sync/atomic"
"time"
)
// globalConfig 保存当前配置,支持原子访问
var globalConfig atomic.Pointer[Config]
// Config 应用配置
type Config struct {
Server ServerConfig `mapstructure:"server"`
Redis RedisConfig `mapstructure:"redis"`
Database DatabaseConfig `mapstructure:"database"`
Queue QueueConfig `mapstructure:"queue"`
Logging LoggingConfig `mapstructure:"logging"`
Middleware MiddlewareConfig `mapstructure:"middleware"`
}
// ServerConfig HTTP 服务器配置
type ServerConfig struct {
Address string `mapstructure:"address"` // 例如 ":3000"
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 例如 "10s"
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 例如 "10s"
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` // 例如 "30s"
Prefork bool `mapstructure:"prefork"` // 多进程模式
}
// RedisConfig Redis 连接配置
type RedisConfig struct {
Address string `mapstructure:"address"` // 例如 "localhost"
Port int `mapstructure:"port"` // 例如 "6379"
Password string `mapstructure:"password"` // 无认证时留空
DB int `mapstructure:"db"` // 数据库编号0-15
PoolSize int `mapstructure:"pool_size"` // 最大连接数
MinIdleConns int `mapstructure:"min_idle_conns"` // 保活连接数
DialTimeout time.Duration `mapstructure:"dial_timeout"` // 例如 "5s"
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 例如 "3s"
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 例如 "3s"
}
// DatabaseConfig 数据库连接配置
type DatabaseConfig struct {
Host string `mapstructure:"host"` // 数据库主机地址
Port int `mapstructure:"port"` // 数据库端口
User string `mapstructure:"user"` // 数据库用户名
Password string `mapstructure:"password"` // 数据库密码(明文存储)
DBName string `mapstructure:"dbname"` // 数据库名称
SSLMode string `mapstructure:"sslmode"` // SSL 模式disable, require, verify-ca, verify-full
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数默认25
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数默认10
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生命周期默认5m
}
// QueueConfig 任务队列配置
type QueueConfig struct {
Concurrency int `mapstructure:"concurrency"` // Worker 并发数默认10
Queues map[string]int `mapstructure:"queues"` // 队列优先级配置(队列名 -> 权重)
RetryMax int `mapstructure:"retry_max"` // 最大重试次数默认5
Timeout time.Duration `mapstructure:"timeout"` // 任务超时时间默认10m
}
// LoggingConfig 日志配置
type LoggingConfig struct {
Level string `mapstructure:"level"` // debug, info, warn, error
Development bool `mapstructure:"development"` // 启用开发模式(美化输出)
AppLog LogRotationConfig `mapstructure:"app_log"` // 应用日志配置
AccessLog LogRotationConfig `mapstructure:"access_log"` // HTTP 访问日志配置
}
// LogRotationConfig Lumberjack 日志轮转配置
type LogRotationConfig struct {
Filename string `mapstructure:"filename"` // 日志文件路径
MaxSize int `mapstructure:"max_size"` // 轮转前的最大大小MB
MaxBackups int `mapstructure:"max_backups"` // 保留的旧文件最大数量
MaxAge int `mapstructure:"max_age"` // 保留旧文件的最大天数
Compress bool `mapstructure:"compress"` // 压缩轮转的文件
}
// MiddlewareConfig 中间件配置
type MiddlewareConfig struct {
EnableAuth bool `mapstructure:"enable_auth"` // 启用 keyauth 中间件
EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // 启用限流器默认false
RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // 限流器配置
}
// RateLimiterConfig 限流器配置
type RateLimiterConfig struct {
Max int `mapstructure:"max"` // 时间窗口内的最大请求数
Expiration time.Duration `mapstructure:"expiration"` // 时间窗口(例如 "1m"
Storage string `mapstructure:"storage"` // "memory" 或 "redis"
}
// Validate 验证配置值
func (c *Config) Validate() error {
// 服务器验证
if c.Server.Address == "" {
return fmt.Errorf("invalid configuration: server.address: must be non-empty (current value: empty)")
}
if c.Server.ReadTimeout < 5*time.Second || c.Server.ReadTimeout > 300*time.Second {
return fmt.Errorf("invalid configuration: server.read_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.ReadTimeout)
}
if c.Server.WriteTimeout < 5*time.Second || c.Server.WriteTimeout > 300*time.Second {
return fmt.Errorf("invalid configuration: server.write_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.WriteTimeout)
}
if c.Server.ShutdownTimeout < 10*time.Second || c.Server.ShutdownTimeout > 120*time.Second {
return fmt.Errorf("invalid configuration: server.shutdown_timeout: duration out of range (current value: %s, expected: 10s-120s)", c.Server.ShutdownTimeout)
}
// Redis 验证
if c.Redis.Address == "" {
return fmt.Errorf("invalid configuration: redis.address: must be non-empty (current value: empty)")
}
// Port 验证(独立字段)
if c.Redis.Port <= 0 || c.Redis.Port > 65535 {
return fmt.Errorf("invalid configuration: redis.port: port number out of range (current value: %d, expected: 1-65535)", c.Redis.Port)
}
if c.Redis.DB < 0 || c.Redis.DB > 15 {
return fmt.Errorf("invalid configuration: redis.db: database number out of range (current value: %d, expected: 0-15)", c.Redis.DB)
}
if c.Redis.PoolSize <= 0 || c.Redis.PoolSize > 1000 {
return fmt.Errorf("invalid configuration: redis.pool_size: pool size out of range (current value: %d, expected: 1-1000)", c.Redis.PoolSize)
}
if c.Redis.MinIdleConns < 0 || c.Redis.MinIdleConns > c.Redis.PoolSize {
return fmt.Errorf("invalid configuration: redis.min_idle_conns: must be 0 to pool_size (current value: %d, pool_size: %d)", c.Redis.MinIdleConns, c.Redis.PoolSize)
}
// 日志验证
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
if !validLevels[c.Logging.Level] {
return fmt.Errorf("invalid configuration: logging.level: invalid log level (current value: %s, expected: debug, info, warn, error)", c.Logging.Level)
}
if c.Logging.AppLog.Filename == "" {
return fmt.Errorf("invalid configuration: logging.app_log.filename: must be non-empty valid file path")
}
if c.Logging.AccessLog.Filename == "" {
return fmt.Errorf("invalid configuration: logging.access_log.filename: must be non-empty valid file path")
}
if c.Logging.AppLog.MaxSize < 1 || c.Logging.AppLog.MaxSize > 1000 {
return fmt.Errorf("invalid configuration: logging.app_log.max_size: size out of range (current value: %d, expected: 1-1000 MB)", c.Logging.AppLog.MaxSize)
}
if c.Logging.AccessLog.MaxSize < 1 || c.Logging.AccessLog.MaxSize > 1000 {
return fmt.Errorf("invalid configuration: logging.access_log.max_size: size out of range (current value: %d, expected: 1-1000 MB)", c.Logging.AccessLog.MaxSize)
}
// 中间件验证
if c.Middleware.RateLimiter.Max <= 0 {
c.Middleware.RateLimiter.Max = 100 // 默认值
}
if c.Middleware.RateLimiter.Max > 10000 {
return fmt.Errorf("invalid configuration: middleware.rate_limiter.max: request limit out of range (current value: %d, expected: 1-10000)", c.Middleware.RateLimiter.Max)
}
validStorage := map[string]bool{"memory": true, "redis": true}
if c.Middleware.RateLimiter.Storage != "" && !validStorage[c.Middleware.RateLimiter.Storage] {
return fmt.Errorf("invalid configuration: middleware.rate_limiter.storage: invalid storage type (current value: %s, expected: memory or redis)", c.Middleware.RateLimiter.Storage)
}
return nil
}
// Get 返回当前配置
func Get() *Config {
return globalConfig.Load()
}
// Set 原子地更新全局配置
func Set(cfg *Config) error {
if cfg == nil {
return errors.New("config cannot be nil")
}
if err := cfg.Validate(); err != nil {
return err
}
globalConfig.Store(cfg)
return nil
}