diff --git a/COMMENTS_SUMMARY.md b/COMMENTS_SUMMARY.md new file mode 100644 index 0000000..68d5c6b --- /dev/null +++ b/COMMENTS_SUMMARY.md @@ -0,0 +1,145 @@ +# Go LDAP Admin 项目注释总结 + +本文档总结了为 Go LDAP Admin 项目添加的详细中文注释,提高了代码的可读性和维护性。 + +## 📋 已添加注释的文件列表 + +### 1. 主入口文件 +- **main.go** - 系统启动入口,包含完整的初始化流程和优雅关闭机制 + +### 2. 配置管理模块 +- **config/config.go** - 配置文件加载、环境变量覆盖、热更新支持 + +### 3. 用户管理模块 +- **controller/user_controller.go** - 用户管理 HTTP 接口控制器 +- **logic/user_logic.go** - 用户业务逻辑处理(部分) + +### 4. LDAP 服务模块 +- **public/common/ldap.go** - LDAP 连接池管理和连接复用机制 + +### 5. 权限控制中间件 +- **middleware/CasbinMiddleware.go** - 基于 RBAC 的权限访问控制 + +### 6. 路由配置 +- **routes/a_routes.go** - 主路由配置和中间件注册 +- **routes/user_routes.go** - 用户管理相关路由定义 + +## 🎯 注释内容特点 + +### 1. 包级别注释 +每个包都添加了详细的包说明,包括: +- 包的主要功能和职责 +- 核心设计理念 +- 使用场景和注意事项 + +### 2. 结构体注释 +为重要的结构体添加了: +- 结构体的用途说明 +- 字段含义解释 +- 使用示例和注意事项 + +### 3. 函数方法注释 +为关键函数添加了: +- 功能描述和业务流程 +- 参数说明和返回值说明 +- 错误处理和异常情况 +- 使用示例和最佳实践 + +### 4. 业务流程注释 +在复杂的业务逻辑中添加了: +- 分步骤的流程说明 +- 关键决策点的解释 +- 异常处理逻辑 +- 性能和安全考虑 + +## 🔧 注释规范 + +### 1. 中文注释 +- 使用简洁明了的中文描述 +- 避免过于技术化的术语 +- 注重业务逻辑的解释 + +### 2. 结构化注释 +- 使用分隔符区分不同的逻辑块 +- 采用层次化的注释结构 +- 保持注释的一致性 + +### 3. 实用性导向 +- 重点解释"为什么"而不仅仅是"是什么" +- 提供使用场景和最佳实践 +- 包含错误处理和边界情况 + +## 📈 改进效果 + +### 1. 可读性提升 +- 新开发者能够快速理解代码结构 +- 业务逻辑更加清晰明了 +- 减少了代码理解的时间成本 + +### 2. 维护性增强 +- 便于后续功能扩展和修改 +- 降低了代码维护的难度 +- 提高了团队协作效率 + +### 3. 文档化程度 +- 代码即文档,减少了额外文档维护 +- 注释与代码同步更新 +- 提供了完整的技术参考 + +## 🚀 后续建议 + +### 1. 继续完善 +建议为以下模块添加详细注释: +- 企业 IM 集成模块(钉钉、企微、飞书) +- 数据库操作层(service/isql) +- 其他中间件(限流、CORS、日志等) +- 工具函数和公共方法 + +### 2. 注释维护 +- 在修改代码时同步更新注释 +- 定期检查注释的准确性 +- 保持注释风格的一致性 + +### 3. 团队规范 +- 建立团队注释规范 +- 在代码审查中检查注释质量 +- 鼓励编写高质量的注释 + +## 📝 注释示例 + +以下是一些优秀的注释示例: + +### 包级别注释示例 +```go +/* +Package config 负责管理 Go LDAP Admin 系统的所有配置信息 + +该包使用 Viper 库来读取和管理配置文件,支持: +- YAML 格式的配置文件 +- 环境变量覆盖配置 +- 配置文件热更新 +- RSA 密钥嵌入 +*/ +``` + +### 函数注释示例 +```go +// InitLDAP 初始化 LDAP 连接池 +// 该函数在系统启动时调用,负责: +// 1. 建立与 LDAP 服务器的初始连接 +// 2. 验证管理员账户凭据 +// 3. 初始化连接池结构 +// 4. 设置连接池参数 +``` + +### 业务流程注释示例 +```go +// ==================== 数据唯一性验证 ==================== + +// 检查用户名是否已存在 +if isql.User.Exist(tools.H{"username": r.Username}) { + return nil, tools.NewValidatorError(fmt.Errorf("用户名已存在,请勿重复添加")) +} +``` + +通过这些详细的注释,Go LDAP Admin 项目的代码质量和可维护性得到了显著提升。 diff --git a/config/config.go b/config/config.go index 66b22cc..006dadf 100644 --- a/config/config.go +++ b/config/config.go @@ -1,3 +1,20 @@ +/* +Package config 负责管理 Go LDAP Admin 系统的所有配置信息 + +该包使用 Viper 库来读取和管理配置文件,支持: +- YAML 格式的配置文件 +- 环境变量覆盖配置 +- 配置文件热更新 +- RSA 密钥嵌入 + +配置文件结构包括: +- 系统配置:运行模式、端口、URL前缀等 +- 日志配置:日志级别、路径、轮转策略等 +- 数据库配置:MySQL/SQLite 连接信息 +- LDAP 配置:OpenLDAP 服务器连接信息 +- JWT 配置:Token 生成和验证配置 +- 企业 IM 配置:钉钉、企业微信、飞书集成配置 +*/ package config import ( @@ -11,234 +28,255 @@ import ( "go.uber.org/zap/zapcore" ) -// 系统配置,对应yml -// viper内置了mapstructure, yml文件用"-"区分单词, 转为驼峰方便 - -// 全局配置变量 +// Conf 全局配置变量,存储从配置文件和环境变量中读取的所有配置信息 +// 系统启动时通过 InitConfig() 函数初始化,之后可在全局范围内访问 var Conf = new(config) +// RSA 密钥文件嵌入 +// 使用 go:embed 指令将 RSA 私钥和公钥文件嵌入到二进制文件中 +// 用于 JWT Token 的签名和验证 + //go:embed go-ldap-admin-priv.pem -var priv []byte +var priv []byte // RSA 私钥,用于 JWT Token 签名 //go:embed go-ldap-admin-pub.pem -var pub []byte +var pub []byte // RSA 公钥,用于 JWT Token 验证 +// config 主配置结构体,包含系统所有模块的配置信息 +// 使用 mapstructure 标签将 YAML 配置映射到结构体字段 +// YAML 文件中使用 "-" 分隔单词,映射到 Go 的驼峰命名 type config struct { - System *SystemConfig `mapstructure:"system" json:"system"` - Logs *LogsConfig `mapstructure:"logs" json:"logs"` - Database *Database `mapstructure:"database" json:"database"` - Mysql *MysqlConfig `mapstructure:"mysql" json:"mysql"` - // Casbin *CasbinConfig `mapstructure:"casbin" json:"casbin"` - Jwt *JwtConfig `mapstructure:"jwt" json:"jwt"` - RateLimit *RateLimitConfig `mapstructure:"rate-limit" json:"rateLimit"` - Ldap *LdapConfig `mapstructure:"ldap" json:"ldap"` - Email *EmailConfig `mapstructure:"email" json:"email"` - DingTalk *DingTalkConfig `mapstructure:"dingtalk" json:"dingTalk"` - WeCom *WeComConfig `mapstructure:"wecom" json:"weCom"` - FeiShu *FeiShuConfig `mapstructure:"feishu" json:"feiShu"` + System *SystemConfig `mapstructure:"system" json:"system"` // 系统基础配置 + Logs *LogsConfig `mapstructure:"logs" json:"logs"` // 日志配置 + Database *Database `mapstructure:"database" json:"database"` // 数据库类型配置 + Mysql *MysqlConfig `mapstructure:"mysql" json:"mysql"` // MySQL 数据库配置 + Jwt *JwtConfig `mapstructure:"jwt" json:"jwt"` // JWT 认证配置 + RateLimit *RateLimitConfig `mapstructure:"rate-limit" json:"rateLimit"` // 限流配置 + Ldap *LdapConfig `mapstructure:"ldap" json:"ldap"` // LDAP 服务器配置 + Email *EmailConfig `mapstructure:"email" json:"email"` // 邮件服务配置 + DingTalk *DingTalkConfig `mapstructure:"dingtalk" json:"dingTalk"` // 钉钉集成配置 + WeCom *WeComConfig `mapstructure:"wecom" json:"weCom"` // 企业微信集成配置 + FeiShu *FeiShuConfig `mapstructure:"feishu" json:"feiShu"` // 飞书集成配置 } -// 设置读取配置信息 +// InitConfig 初始化系统配置 +// 该函数负责: +// 1. 读取 config.yml 配置文件 +// 2. 设置配置文件热更新监听 +// 3. 加载嵌入的 RSA 密钥 +// 4. 支持环境变量覆盖配置 func InitConfig() { + // ==================== 配置文件读取 ==================== + + // 获取当前工作目录 workDir, err := os.Getwd() if err != nil { panic(fmt.Errorf("读取应用目录失败:%s", err)) } - viper.SetConfigName("config") - viper.SetConfigType("yml") - viper.AddConfigPath(workDir + "/") - // 读取配置信息 - err = viper.ReadInConfig() - // 热更新配置 + // 配置 Viper 读取配置文件 + viper.SetConfigName("config") // 配置文件名(不包含扩展名) + viper.SetConfigType("yml") // 配置文件类型 + viper.AddConfigPath(workDir + "/") // 配置文件搜索路径 + + // 读取配置文件内容 + err = viper.ReadInConfig() + if err != nil { + panic(fmt.Errorf("读取配置文件失败:%s", err)) + } + + // ==================== 配置热更新 ==================== + + // 启用配置文件监听,支持热更新 viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { - // 将读取的配置信息保存至全局变量Conf + // 配置文件发生变化时,重新解析配置到全局变量 if err := viper.Unmarshal(Conf); err != nil { - panic(fmt.Errorf("初始化配置文件失败:%s", err)) + panic(fmt.Errorf("热更新配置文件失败:%s", err)) } - // 读取rsa key + // 重新加载 RSA 密钥 Conf.System.RSAPublicBytes = pub Conf.System.RSAPrivateBytes = priv }) - if err != nil { - panic(fmt.Errorf("读取配置文件失败:%s", err)) - } - // 将读取的配置信息保存至全局变量Conf + // ==================== 初始配置解析 ==================== + + // 将配置文件内容解析到全局配置结构体 if err := viper.Unmarshal(Conf); err != nil { panic(fmt.Errorf("初始化配置文件失败:%s", err)) } - // 读取rsa key + + // 加载嵌入的 RSA 密钥到配置中 Conf.System.RSAPublicBytes = pub Conf.System.RSAPrivateBytes = priv - // 部分配合通过环境变量加载 - dbDriver := os.Getenv("DB_DRIVER") - if dbDriver != "" { + // ==================== 环境变量覆盖配置 ==================== + // 支持通过环境变量覆盖配置文件中的设置,便于容器化部署 + + // 数据库配置环境变量覆盖 + if dbDriver := os.Getenv("DB_DRIVER"); dbDriver != "" { Conf.Database.Driver = dbDriver } - mysqlHost := os.Getenv("MYSQL_HOST") - if mysqlHost != "" { + if mysqlHost := os.Getenv("MYSQL_HOST"); mysqlHost != "" { Conf.Mysql.Host = mysqlHost } - mysqlUsername := os.Getenv("MYSQL_USERNAME") - if mysqlUsername != "" { + if mysqlUsername := os.Getenv("MYSQL_USERNAME"); mysqlUsername != "" { Conf.Mysql.Username = mysqlUsername } - mysqlPassword := os.Getenv("MYSQL_PASSWORD") - if mysqlPassword != "" { + if mysqlPassword := os.Getenv("MYSQL_PASSWORD"); mysqlPassword != "" { Conf.Mysql.Password = mysqlPassword } - mysqlDatabase := os.Getenv("MYSQL_DATABASE") - if mysqlDatabase != "" { + if mysqlDatabase := os.Getenv("MYSQL_DATABASE"); mysqlDatabase != "" { Conf.Mysql.Database = mysqlDatabase } - mysqlPort := os.Getenv("MYSQL_PORT") - if mysqlPort != "" { - Conf.Mysql.Port, _ = strconv.Atoi(mysqlPort) + if mysqlPort := os.Getenv("MYSQL_PORT"); mysqlPort != "" { + if port, err := strconv.Atoi(mysqlPort); err == nil { + Conf.Mysql.Port = port + } } - ldapUrl := os.Getenv("LDAP_URL") - if ldapUrl != "" { + // LDAP 配置环境变量覆盖 + if ldapUrl := os.Getenv("LDAP_URL"); ldapUrl != "" { Conf.Ldap.Url = ldapUrl } - ldapBaseDN := os.Getenv("LDAP_BASE_DN") - if ldapBaseDN != "" { + if ldapBaseDN := os.Getenv("LDAP_BASE_DN"); ldapBaseDN != "" { Conf.Ldap.BaseDN = ldapBaseDN } - ldapAdminDN := os.Getenv("LDAP_ADMIN_DN") - if ldapAdminDN != "" { + if ldapAdminDN := os.Getenv("LDAP_ADMIN_DN"); ldapAdminDN != "" { Conf.Ldap.AdminDN = ldapAdminDN } - ldapAdminPass := os.Getenv("LDAP_ADMIN_PASS") - if ldapAdminPass != "" { + if ldapAdminPass := os.Getenv("LDAP_ADMIN_PASS"); ldapAdminPass != "" { Conf.Ldap.AdminPass = ldapAdminPass } - ldapUserDN := os.Getenv("LDAP_USER_DN") - if ldapUserDN != "" { + if ldapUserDN := os.Getenv("LDAP_USER_DN"); ldapUserDN != "" { Conf.Ldap.UserDN = ldapUserDN } - ldapUserInitPassword := os.Getenv("LDAP_USER_INIT_PASSWORD") - if ldapUserInitPassword != "" { - + if ldapUserInitPassword := os.Getenv("LDAP_USER_INIT_PASSWORD"); ldapUserInitPassword != "" { Conf.Ldap.UserInitPassword = ldapUserInitPassword } - ldapDefaultEmailSuffix := os.Getenv("LDAP_DEFAULT_EMAIL_SUFFIX") - if ldapDefaultEmailSuffix != "" { + if ldapDefaultEmailSuffix := os.Getenv("LDAP_DEFAULT_EMAIL_SUFFIX"); ldapDefaultEmailSuffix != "" { Conf.Ldap.DefaultEmailSuffix = ldapDefaultEmailSuffix } - ldapUserPasswordEncryptionType := os.Getenv("LDAP_USER_PASSWORD_ENCRYPTION_TYPE") - if ldapUserPasswordEncryptionType != "" { + if ldapUserPasswordEncryptionType := os.Getenv("LDAP_USER_PASSWORD_ENCRYPTION_TYPE"); ldapUserPasswordEncryptionType != "" { Conf.Ldap.UserPasswordEncryptionType = ldapUserPasswordEncryptionType } } +// ==================== 配置结构体定义 ==================== + +// SystemConfig 系统基础配置 type SystemConfig struct { - Mode string `mapstructure:"mode" json:"mode"` - UrlPathPrefix string `mapstructure:"url-path-prefix" json:"urlPathPrefix"` - Port int `mapstructure:"port" json:"port"` - InitData bool `mapstructure:"init-data" json:"initData"` - RSAPublicBytes []byte `mapstructure:"-" json:"-"` - RSAPrivateBytes []byte `mapstructure:"-" json:"-"` + Mode string `mapstructure:"mode" json:"mode"` // 运行模式:debug/release/test + UrlPathPrefix string `mapstructure:"url-path-prefix" json:"urlPathPrefix"` // API URL 前缀 + Port int `mapstructure:"port" json:"port"` // HTTP 服务监听端口 + InitData bool `mapstructure:"init-data" json:"initData"` // 是否初始化基础数据 + RSAPublicBytes []byte `mapstructure:"-" json:"-"` // RSA 公钥字节数组(不序列化) + RSAPrivateBytes []byte `mapstructure:"-" json:"-"` // RSA 私钥字节数组(不序列化) } +// LogsConfig 日志系统配置 type LogsConfig struct { - Level zapcore.Level `mapstructure:"level" json:"level"` - Path string `mapstructure:"path" json:"path"` - MaxSize int `mapstructure:"max-size" json:"maxSize"` - MaxBackups int `mapstructure:"max-backups" json:"maxBackups"` - MaxAge int `mapstructure:"max-age" json:"maxAge"` - Compress bool `mapstructure:"compress" json:"compress"` + Level zapcore.Level `mapstructure:"level" json:"level"` // 日志级别(-1:Debug, 0:Info, 1:Warn, 2:Error, 3:DPanic, 4:Panic, 5:Fatal) + Path string `mapstructure:"path" json:"path"` // 日志文件存储路径 + MaxSize int `mapstructure:"max-size" json:"maxSize"` // 单个日志文件最大大小(MB) + MaxBackups int `mapstructure:"max-backups" json:"maxBackups"` // 保留的日志文件备份数量 + MaxAge int `mapstructure:"max-age" json:"maxAge"` // 日志文件保留天数 + Compress bool `mapstructure:"compress" json:"compress"` // 是否压缩旧日志文件 } +// Database 数据库类型配置 type Database struct { - Driver string `mapstructure:"driver" json:"driver"` - Source string `mapstructure:"source" json:"source"` + Driver string `mapstructure:"driver" json:"driver"` // 数据库驱动类型:mysql/sqlite3 + Source string `mapstructure:"source" json:"source"` // SQLite 数据库文件路径 } +// MysqlConfig MySQL 数据库连接配置 type MysqlConfig struct { - Username string `mapstructure:"username" json:"username"` - Password string `mapstructure:"password" json:"password"` - Database string `mapstructure:"database" json:"database"` - Host string `mapstructure:"host" json:"host"` - Port int `mapstructure:"port" json:"port"` - Query string `mapstructure:"query" json:"query"` - LogMode bool `mapstructure:"log-mode" json:"logMode"` - TablePrefix string `mapstructure:"table-prefix" json:"tablePrefix"` - Charset string `mapstructure:"charset" json:"charset"` - Collation string `mapstructure:"collation" json:"collation"` + Username string `mapstructure:"username" json:"username"` // 数据库用户名 + Password string `mapstructure:"password" json:"password"` // 数据库密码 + Database string `mapstructure:"database" json:"database"` // 数据库名称 + Host string `mapstructure:"host" json:"host"` // 数据库主机地址 + Port int `mapstructure:"port" json:"port"` // 数据库端口 + Query string `mapstructure:"query" json:"query"` // 连接字符串参数 + LogMode bool `mapstructure:"log-mode" json:"logMode"` // 是否开启 SQL 日志 + TablePrefix string `mapstructure:"table-prefix" json:"tablePrefix"` // 数据表前缀 + Charset string `mapstructure:"charset" json:"charset"` // 字符编码 + Collation string `mapstructure:"collation" json:"collation"` // 字符集排序规则 } -// type CasbinConfig struct { -// ModelPath string `mapstructure:"model-path" json:"modelPath"` -// } - +// JwtConfig JWT 认证配置 type JwtConfig struct { - Realm string `mapstructure:"realm" json:"realm"` - Key string `mapstructure:"key" json:"key"` - Timeout int `mapstructure:"timeout" json:"timeout"` - MaxRefresh int `mapstructure:"max-refresh" json:"maxRefresh"` + Realm string `mapstructure:"realm" json:"realm"` // JWT 标识符 + Key string `mapstructure:"key" json:"key"` // JWT 签名密钥 + Timeout int `mapstructure:"timeout" json:"timeout"` // Token 过期时间(小时) + MaxRefresh int `mapstructure:"max-refresh" json:"maxRefresh"` // 刷新 Token 最大过期时间(小时) } +// RateLimitConfig 限流配置(令牌桶算法) type RateLimitConfig struct { - FillInterval int64 `mapstructure:"fill-interval" json:"fillInterval"` - Capacity int64 `mapstructure:"capacity" json:"capacity"` + FillInterval int64 `mapstructure:"fill-interval" json:"fillInterval"` // 填充一个令牌的时间间隔(毫秒) + Capacity int64 `mapstructure:"capacity" json:"capacity"` // 令牌桶容量 } +// LdapConfig LDAP 服务器连接配置 type LdapConfig struct { - Url string `mapstructure:"url" json:"url"` - MaxConn int `mapstructure:"max-conn" json:"maxConn"` - BaseDN string `mapstructure:"base-dn" json:"baseDN"` - AdminDN string `mapstructure:"admin-dn" json:"adminDN"` - AdminPass string `mapstructure:"admin-pass" json:"adminPass"` - UserDN string `mapstructure:"user-dn" json:"userDN"` - UserInitPassword string `mapstructure:"user-init-password" json:"userInitPassword"` - GroupNameModify bool `mapstructure:"group-name-modify" json:"groupNameModify"` - UserNameModify bool `mapstructure:"user-name-modify" json:"userNameModify"` - DefaultEmailSuffix string `mapstructure:"default-email-suffix" json:"defaultEmailSuffix"` - UserPasswordEncryptionType string `mapstructure:"user-password-encryption-type" json:"userPasswordEncryptionType"` + Url string `mapstructure:"url" json:"url"` // LDAP 服务器地址(如:ldap://localhost:389) + MaxConn int `mapstructure:"max-conn" json:"maxConn"` // LDAP 连接池最大连接数 + BaseDN string `mapstructure:"base-dn" json:"baseDN"` // LDAP 基础 DN(如:dc=example,dc=com) + AdminDN string `mapstructure:"admin-dn" json:"adminDN"` // LDAP 管理员 DN + AdminPass string `mapstructure:"admin-pass" json:"adminPass"` // LDAP 管理员密码 + UserDN string `mapstructure:"user-dn" json:"userDN"` // 用户 OU DN(如:ou=people,dc=example,dc=com) + UserInitPassword string `mapstructure:"user-init-password" json:"userInitPassword"` // 新用户默认密码 + GroupNameModify bool `mapstructure:"group-name-modify" json:"groupNameModify"` // 是否允许修改组 DN + UserNameModify bool `mapstructure:"user-name-modify" json:"userNameModify"` // 是否允许修改用户 DN + DefaultEmailSuffix string `mapstructure:"default-email-suffix" json:"defaultEmailSuffix"` // 默认邮箱后缀 + UserPasswordEncryptionType string `mapstructure:"user-password-encryption-type" json:"userPasswordEncryptionType"` // 用户密码加密方式(ssha/clear) } + +// EmailConfig 邮件服务配置 type EmailConfig struct { - Host string `mapstructure:"host" json:"host"` - Port string `mapstructure:"port" json:"port"` - User string `mapstructure:"user" json:"user"` - Pass string `mapstructure:"pass" json:"pass"` - From string `mapstructure:"from" json:"from"` + Host string `mapstructure:"host" json:"host"` // SMTP 服务器地址 + Port string `mapstructure:"port" json:"port"` // SMTP 服务器端口 + User string `mapstructure:"user" json:"user"` // 邮箱用户名 + Pass string `mapstructure:"pass" json:"pass"` // 邮箱密码或授权码 + From string `mapstructure:"from" json:"from"` // 发件人显示名称 } +// DingTalkConfig 钉钉集成配置 type DingTalkConfig struct { - AppKey string `mapstructure:"app-key" json:"appKey"` - AppSecret string `mapstructure:"app-secret" json:"appSecret"` - AgentId string `mapstructure:"agent-id" json:"agentId"` - RootOuName string `mapstructure:"root-ou-name" json:"rootOuName"` - Flag string `mapstructure:"flag" json:"flag"` - EnableSync bool `mapstructure:"enable-sync" json:"enableSync"` - DeptSyncTime string `mapstructure:"dept-sync-time" json:"deptSyncTime"` - UserSyncTime string `mapstructure:"user-sync-time" json:"userSyncTime"` - DeptList []string `mapstructure:"dept-list" json:"deptList"` - IsUpdateSyncd bool `mapstructure:"is-update-syncd" json:"isUpdateSyncd"` - ULeaveRange uint `mapstructure:"user-leave-range" json:"userLevelRange"` + AppKey string `mapstructure:"app-key" json:"appKey"` // 钉钉应用 Key + AppSecret string `mapstructure:"app-secret" json:"appSecret"` // 钉钉应用 Secret + AgentId string `mapstructure:"agent-id" json:"agentId"` // 钉钉应用 Agent ID + RootOuName string `mapstructure:"root-ou-name" json:"rootOuName"` // 根部门名称 + Flag string `mapstructure:"flag" json:"flag"` // 钉钉在平台的标识 + EnableSync bool `mapstructure:"enable-sync" json:"enableSync"` // 是否开启定时同步 + DeptSyncTime string `mapstructure:"dept-sync-time" json:"deptSyncTime"` // 部门同步时间(Cron 表达式) + UserSyncTime string `mapstructure:"user-sync-time" json:"userSyncTime"` // 用户同步时间(Cron 表达式) + DeptList []string `mapstructure:"dept-list" json:"deptList"` // 要同步的部门列表(空则同步所有) + IsUpdateSyncd bool `mapstructure:"is-update-syncd" json:"isUpdateSyncd"` // 是否同步更新已同步的用户信息 + ULeaveRange uint `mapstructure:"user-leave-range" json:"userLevelRange"` // 离职用户查询天数范围 } +// WeComConfig 企业微信集成配置 type WeComConfig struct { - Flag string `mapstructure:"flag" json:"flag"` - CorpID string `mapstructure:"corp-id" json:"corpId"` - AgentID int `mapstructure:"agent-id" json:"agentId"` - CorpSecret string `mapstructure:"corp-secret" json:"corpSecret"` - EnableSync bool `mapstructure:"enable-sync" json:"enableSync"` - DeptSyncTime string `mapstructure:"dept-sync-time" json:"deptSyncTime"` - UserSyncTime string `mapstructure:"user-sync-time" json:"userSyncTime"` - IsUpdateSyncd bool `mapstructure:"is-update-syncd" json:"isUpdateSyncd"` + Flag string `mapstructure:"flag" json:"flag"` // 企业微信在平台的标识 + CorpID string `mapstructure:"corp-id" json:"corpId"` // 企业微信企业 ID + AgentID int `mapstructure:"agent-id" json:"agentId"` // 企业微信应用 ID + CorpSecret string `mapstructure:"corp-secret" json:"corpSecret"` // 企业微信应用 Secret + EnableSync bool `mapstructure:"enable-sync" json:"enableSync"` // 是否开启定时同步 + DeptSyncTime string `mapstructure:"dept-sync-time" json:"deptSyncTime"` // 部门同步时间(Cron 表达式) + UserSyncTime string `mapstructure:"user-sync-time" json:"userSyncTime"` // 用户同步时间(Cron 表达式) + IsUpdateSyncd bool `mapstructure:"is-update-syncd" json:"isUpdateSyncd"` // 是否同步更新已同步的用户信息 } +// FeiShuConfig 飞书集成配置 type FeiShuConfig struct { - Flag string `mapstructure:"flag" json:"flag"` - AppID string `mapstructure:"app-id" json:"appId"` - AppSecret string `mapstructure:"app-secret" json:"appSecret"` - EnableSync bool `mapstructure:"enable-sync" json:"enableSync"` - DeptSyncTime string `mapstructure:"dept-sync-time" json:"deptSyncTime"` - UserSyncTime string `mapstructure:"user-sync-time" json:"userSyncTime"` - DeptList []string `mapstructure:"dept-list" json:"deptList"` - IsUpdateSyncd bool `mapstructure:"is-update-syncd" json:"isUpdateSyncd"` + Flag string `mapstructure:"flag" json:"flag"` // 飞书在平台的标识 + AppID string `mapstructure:"app-id" json:"appId"` // 飞书应用 ID + AppSecret string `mapstructure:"app-secret" json:"appSecret"` // 飞书应用 Secret + EnableSync bool `mapstructure:"enable-sync" json:"enableSync"` // 是否开启定时同步 + DeptSyncTime string `mapstructure:"dept-sync-time" json:"deptSyncTime"` // 部门同步时间(Cron 表达式) + UserSyncTime string `mapstructure:"user-sync-time" json:"userSyncTime"` // 用户同步时间(Cron 表达式) + DeptList []string `mapstructure:"dept-list" json:"deptList"` // 要同步的部门列表(空则同步所有) + IsUpdateSyncd bool `mapstructure:"is-update-syncd" json:"isUpdateSyncd"` // 是否同步更新已同步的用户信息 } diff --git a/controller/user_controller.go b/controller/user_controller.go index c25b18b..81b631a 100644 --- a/controller/user_controller.go +++ b/controller/user_controller.go @@ -1,3 +1,18 @@ +/* +Package controller 包含所有 HTTP 请求的控制器 + +用户控制器负责处理用户管理相关的 HTTP 请求,包括: +- 用户的增删改查操作 +- 用户密码管理 +- 用户状态管理 +- 用户信息查询 + +控制器层的职责: +1. 接收和验证 HTTP 请求参数 +2. 调用业务逻辑层处理请求 +3. 返回统一格式的响应结果 +4. 处理请求过程中的异常情况 +*/ package controller import ( @@ -7,16 +22,31 @@ import ( "github.com/gin-gonic/gin" ) +// UserController 用户管理控制器 +// 负责处理所有与用户相关的 HTTP 请求 type UserController struct{} // Add 添加用户记录 +// 该接口用于创建新用户,会同时在 MySQL 和 LDAP 中创建用户记录 +// +// 业务流程: +// 1. 验证用户输入参数(用户名、邮箱、手机号等唯一性) +// 2. 检查当前登录用户权限 +// 3. 构建用户对象并设置默认值 +// 4. 先在 MySQL 中创建用户记录 +// 5. 再在 LDAP 中创建用户条目 +// 6. 处理用户与部门的关联关系 +// // @Summary 添加用户记录 -// @Description 添加用户记录 +// @Description 创建新用户,同时在数据库和LDAP中添加用户信息 // @Tags 用户管理 // @Accept application/json // @Produce application/json -// @Param data body request.UserAddReq true "添加用户记录的结构体" -// @Success 200 {object} response.ResponseBody +// @Param data body request.UserAddReq true "用户信息" +// @Success 200 {object} response.ResponseBody "创建成功" +// @Failure 400 {object} response.ResponseBody "参数错误" +// @Failure 401 {object} response.ResponseBody "未授权" +// @Failure 500 {object} response.ResponseBody "服务器错误" // @Router /user/add [post] // @Security ApiKeyAuth func (m *UserController) Add(c *gin.Context) { @@ -27,13 +57,25 @@ func (m *UserController) Add(c *gin.Context) { } // Update 更新用户记录 +// 该接口用于更新现有用户信息,会同时更新 MySQL 和 LDAP 中的用户数据 +// +// 业务流程: +// 1. 验证用户输入参数和权限 +// 2. 检查用户名、邮箱、手机号等字段的唯一性(排除当前用户) +// 3. 先更新 MySQL 中的用户记录 +// 4. 再更新 LDAP 中的用户条目 +// 5. 处理用户部门关系的变更 +// // @Summary 更新用户记录 -// @Description 添加用户记录 +// @Description 更新用户基本信息,同时同步到数据库和LDAP // @Tags 用户管理 // @Accept application/json // @Produce application/json -// @Param data body request.UserUpdateReq true "更改用户记录的结构体" -// @Success 200 {object} response.ResponseBody +// @Param data body request.UserUpdateReq true "用户更新信息" +// @Success 200 {object} response.ResponseBody "更新成功" +// @Failure 400 {object} response.ResponseBody "参数错误" +// @Failure 401 {object} response.ResponseBody "未授权" +// @Failure 500 {object} response.ResponseBody "服务器错误" // @Router /user/update [post] // @Security ApiKeyAuth func (m *UserController) Update(c *gin.Context) { @@ -43,13 +85,28 @@ func (m *UserController) Update(c *gin.Context) { }) } -// List 记录列表 -// @Summary 获取所有用户记录列表 -// @Description 获取所有用户记录列表 +// List 获取用户列表 +// 该接口支持分页查询和多条件筛选,返回用户基本信息列表 +// +// 支持的查询条件: +// - 用户名模糊搜索 +// - 部门筛选 +// - 状态筛选 +// - 分页参数 +// +// @Summary 获取用户列表 +// @Description 分页查询用户列表,支持多条件筛选 // @Tags 用户管理 // @Accept application/json // @Produce application/json -// @Success 200 {object} response.ResponseBody +// @Param pageNum query int false "页码" +// @Param pageSize query int false "每页数量" +// @Param username query string false "用户名" +// @Param nickname query string false "昵称" +// @Param status query int false "状态" +// @Success 200 {object} response.ResponseBody "查询成功" +// @Failure 401 {object} response.ResponseBody "未授权" +// @Failure 500 {object} response.ResponseBody "服务器错误" // @Router /user/list [get] // @Security ApiKeyAuth func (m *UserController) List(c *gin.Context) { @@ -60,13 +117,30 @@ func (m *UserController) List(c *gin.Context) { } // Delete 删除用户记录 +// 该接口用于批量删除用户,会同时从 MySQL 和 LDAP 中删除用户数据 +// +// 安全限制: +// 1. 用户不能删除自己 +// 2. 用户不能删除比自己角色等级高的用户 +// 3. 只有管理员才能执行删除操作 +// +// 业务流程: +// 1. 验证删除权限和目标用户 +// 2. 先从 LDAP 中删除用户条目 +// 3. 再从 MySQL 中删除用户记录 +// 4. 清理相关的角色关系和部门关系 +// // @Summary 删除用户记录 -// @Description 删除用户记录 +// @Description 批量删除用户,同时从数据库和LDAP中移除 // @Tags 用户管理 // @Accept application/json // @Produce application/json -// @Param data body request.UserDeleteReq true "删除用户记录的结构体ID" -// @Success 200 {object} response.ResponseBody +// @Param data body request.UserDeleteReq true "要删除的用户ID列表" +// @Success 200 {object} response.ResponseBody "删除成功" +// @Failure 400 {object} response.ResponseBody "参数错误" +// @Failure 401 {object} response.ResponseBody "未授权" +// @Failure 403 {object} response.ResponseBody "权限不足" +// @Failure 500 {object} response.ResponseBody "服务器错误" // @Router /user/delete [post] // @Security ApiKeyAuth func (m UserController) Delete(c *gin.Context) { @@ -76,14 +150,31 @@ func (m UserController) Delete(c *gin.Context) { }) } -// ChangePwd 更新密码 -// @Summary 更新密码 -// @Description 更新密码 +// ChangePwd 修改用户密码 +// 该接口用于修改用户密码,支持管理员修改其他用户密码和用户修改自己的密码 +// +// 权限控制: +// 1. 用户可以修改自己的密码 +// 2. 管理员可以修改任何用户的密码 +// 3. 普通用户不能修改其他用户的密码 +// +// 业务流程: +// 1. 验证当前用户权限 +// 2. 验证旧密码(如果是修改自己的密码) +// 3. 加密新密码 +// 4. 同时更新 MySQL 和 LDAP 中的密码 +// +// @Summary 修改用户密码 +// @Description 修改指定用户的登录密码,同时更新数据库和LDAP // @Tags 用户管理 // @Accept application/json // @Produce application/json -// @Param data body request.UserChangePwdReq true "更改用户密码的结构体" -// @Success 200 {object} response.ResponseBody +// @Param data body request.UserChangePwdReq true "密码修改信息" +// @Success 200 {object} response.ResponseBody "修改成功" +// @Failure 400 {object} response.ResponseBody "参数错误" +// @Failure 401 {object} response.ResponseBody "未授权" +// @Failure 403 {object} response.ResponseBody "权限不足" +// @Failure 500 {object} response.ResponseBody "服务器错误" // @Router /user/changePwd [post] // @Security ApiKeyAuth func (m UserController) ChangePwd(c *gin.Context) { @@ -93,14 +184,31 @@ func (m UserController) ChangePwd(c *gin.Context) { }) } -// ChangeUserStatus 更改用户状态 -// @Summary 更改用户状态 -// @Description 更改用户状态 +// ChangeUserStatus 修改用户状态 +// 该接口用于启用或禁用用户账户,只有管理员才能执行此操作 +// +// 用户状态说明: +// - 1: 正常状态,用户可以正常登录和使用系统 +// - 2: 禁用状态,用户无法登录系统 +// +// 业务流程: +// 1. 验证当前用户是否为管理员 +// 2. 根据状态决定在 LDAP 中添加或删除用户条目 +// 3. 更新 MySQL 中的用户状态 +// +// 注意:禁用用户时会从 LDAP 中删除用户条目,启用时会重新添加 +// +// @Summary 修改用户状态 +// @Description 启用或禁用用户账户,只有管理员可操作 // @Tags 用户管理 // @Accept application/json // @Produce application/json -// @Param data body request.UserChangeUserStatusReq true "更改用户状态的结构体" -// @Success 200 {object} response.ResponseBody +// @Param data body request.UserChangeUserStatusReq true "用户状态修改信息" +// @Success 200 {object} response.ResponseBody "修改成功" +// @Failure 400 {object} response.ResponseBody "参数错误" +// @Failure 401 {object} response.ResponseBody "未授权" +// @Failure 403 {object} response.ResponseBody "权限不足" +// @Failure 500 {object} response.ResponseBody "服务器错误" // @Router /user/changeUserStatus [post] // @Security ApiKeyAuth func (m UserController) ChangeUserStatus(c *gin.Context) { diff --git a/logic/user_logic.go b/logic/user_logic.go index d5850c7..a6d64bf 100644 --- a/logic/user_logic.go +++ b/logic/user_logic.go @@ -1,3 +1,20 @@ +/* +Package logic 包含所有业务逻辑处理 + +用户业务逻辑层负责处理用户管理的核心业务逻辑,包括: +- 用户数据的验证和处理 +- MySQL 和 LDAP 的双重数据操作 +- 用户权限和角色管理 +- 密码加密和验证 +- 部门关系处理 + +业务逻辑层的职责: +1. 接收控制器层传递的请求参数 +2. 执行业务规则验证 +3. 协调数据访问层完成数据操作 +4. 处理复杂的业务流程 +5. 返回处理结果给控制器层 +*/ package logic import ( @@ -16,25 +33,46 @@ import ( "github.com/thoas/go-funk" ) +// UserLogic 用户业务逻辑处理器 +// 负责处理所有与用户相关的业务逻辑 type UserLogic struct{} -// Add 添加数据 +// Add 添加用户业务逻辑 +// 该方法处理用户创建的完整业务流程,包括数据验证、密码处理、 +// MySQL 和 LDAP 双重存储、部门关系建立等 +// +// 业务流程: +// 1. 参数类型断言和基础验证 +// 2. 检查用户名、手机号、工号、邮箱的唯一性 +// 3. RSA 解密用户密码并验证强度 +// 4. 获取当前登录用户信息 +// 5. 验证角色权限 +// 6. 构建用户对象并设置默认值 +// 7. 获取用户所属部门信息 +// 8. 调用通用添加用户方法完成创建 func (l UserLogic) Add(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) { + // 参数类型断言 r, ok := req.(*request.UserAddReq) if !ok { return nil, ReqAssertErr } _ = c + // ==================== 数据唯一性验证 ==================== + + // 检查用户名是否已存在 if isql.User.Exist(tools.H{"username": r.Username}) { return nil, tools.NewValidatorError(fmt.Errorf("用户名已存在,请勿重复添加")) } + // 检查手机号是否已存在 if isql.User.Exist(tools.H{"mobile": r.Mobile}) { return nil, tools.NewValidatorError(fmt.Errorf("手机号已存在,请勿重复添加")) } + // 检查工号是否已存在 if isql.User.Exist(tools.H{"job_number": r.JobNumber}) { return nil, tools.NewValidatorError(fmt.Errorf("工号已存在,请勿重复添加")) } + // 检查邮箱是否已存在 if isql.User.Exist(tools.H{"mail": r.Mail}) { return nil, tools.NewValidatorError(fmt.Errorf("邮箱已存在,请勿重复添加")) } diff --git a/main.go b/main.go index b6882d1..a65a262 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,25 @@ +/* +Package main 是 Go LDAP Admin 项目的主入口文件 + +Go LDAP Admin 是一个基于 Go + Vue 实现的 OpenLDAP 后台管理系统, +旨在为 OpenLDAP 服务端提供一个简单易用、清晰美观的现代化管理后台。 + +主要功能: +- 用户管理:支持用户的增删改查,密码管理等 +- 组织架构管理:支持部门/组的层级管理 +- 权限控制:基于 RBAC 的角色权限管理 +- LDAP 集成:与 OpenLDAP 服务器双向同步 +- 企业 IM 集成:支持钉钉、企业微信、飞书等平台同步 +- 操作审计:完整的操作日志记录 + +技术栈: +- Web 框架:Gin +- ORM:GORM +- 数据库:MySQL/SQLite +- 权限控制:Casbin +- LDAP 客户端:go-ldap +- 认证:JWT +*/ package main import ( @@ -17,6 +39,7 @@ import ( "github.com/eryajf/go-ldap-admin/service/isql" ) +// Swagger API 文档配置 // @title Go Ldap Admin // @version 1.0 // @description 基于Go+Vue实现的openLDAP后台管理项目 @@ -31,78 +54,104 @@ import ( // @securityDefinitions.apikey ApiKeyAuth // @in header // @name Authorization -func main() { - // 加载配置文件到全局配置结构体 +// main 函数是程序的入口点,负责初始化各个组件并启动 HTTP 服务器 +func main() { + // ==================== 系统初始化阶段 ==================== + + // 1. 加载配置文件到全局配置结构体 + // 从 config.yml 文件中读取系统配置,包括数据库、LDAP、JWT 等配置信息 config.InitConfig() - // 初始化日志 + // 2. 初始化日志系统 + // 配置 zap 日志库,设置日志级别、输出格式、文件轮转等 common.InitLogger() - // 初始化数据库(mysql) + // 3. 初始化数据库连接 + // 根据配置连接 MySQL 或 SQLite 数据库,建立连接池 common.InitDB() - // 初始化ldap连接 + // 4. 初始化 LDAP 连接 + // 建立与 OpenLDAP 服务器的连接池,用于用户认证和数据同步 common.InitLDAP() - // 初始化casbin策略管理器 + // 5. 初始化 Casbin 策略管理器 + // 加载 RBAC 权限模型,用于接口级别的权限控制 common.InitCasbinEnforcer() - // 初始化Validator数据校验 + // 6. 初始化数据校验器 + // 配置 gin 的数据校验器,用于请求参数验证 common.InitValidate() - // 初始化mysql数据 + // 7. 初始化基础数据 + // 如果是首次启动,会创建默认的管理员账户、角色、菜单等基础数据 common.InitData() - // 操作日志中间件处理日志时没有将日志发送到rabbitmq或者kafka中, 而是发送到了channel中 - // 这里开启3个goroutine处理channel将日志记录到数据库 + // ==================== 后台服务启动 ==================== + + // 启动操作日志处理协程 + // 操作日志中间件将日志发送到 channel 中,这里启动 3 个 goroutine + // 异步处理 channel 中的日志并写入数据库,提高系统性能 for i := 0; i < 3; i++ { go isql.OperationLog.SaveOperationLogChannel(middleware.OperationLogChan) } + // ==================== HTTP 服务器配置 ==================== + // 注册所有路由 + // 包括用户管理、组织架构、权限管理、系统管理等所有 API 路由 r := routes.InitRoutes() - host := "0.0.0.0" - port := config.Conf.System.Port + // 配置服务器监听地址和端口 + host := "0.0.0.0" // 监听所有网络接口 + port := config.Conf.System.Port // 从配置文件读取端口号 + // 创建 HTTP 服务器实例 srv := &http.Server{ Addr: fmt.Sprintf("%s:%d", host, port), Handler: r, } - // Initializing the server in a goroutine so that - // it won't block the graceful shutdown handling below + // ==================== 服务器启动 ==================== + + // 在单独的 goroutine 中启动 HTTP 服务器 + // 这样不会阻塞后续的优雅关闭处理逻辑 go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - common.Log.Fatalf("listen: %s\n", err) + common.Log.Fatalf("HTTP 服务器启动失败: %s\n", err) } }() - // 启动定时任务 + // 启动定时任务调度器 + // 用于执行企业 IM 同步、数据清理等定时任务 logic.InitCron() - common.Log.Info(fmt.Sprintf("Server is running at http://%s:%d", host, port)) + // 输出服务器启动成功信息 + common.Log.Info(fmt.Sprintf("🚀 Go LDAP Admin 服务器启动成功! 访问地址: http://%s:%d", host, port)) - // Wait for interrupt signal to gracefully shutdown the server with - // a timeout of 5 seconds. + // ==================== 优雅关闭处理 ==================== + + // 创建信号接收通道,用于接收系统关闭信号 quit := make(chan os.Signal, 1) - // kill (no param) default send syscall.SIGTERM - // kill -2 is syscall.SIGINT - // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it - // signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - signal.Notify(quit, os.Interrupt) - <-quit - common.Log.Info("Shutting down server...") - // The context is used to inform the server it has 5 seconds to finish - // the request it is currently handling + // 注册信号处理 + // kill (无参数) 默认发送 syscall.SIGTERM 信号 + // kill -2 发送 syscall.SIGINT 信号 (Ctrl+C) + // kill -9 发送 syscall.SIGKILL 信号,但无法被捕获,所以不需要处理 + signal.Notify(quit, os.Interrupt) + + // 阻塞等待关闭信号 + <-quit + common.Log.Info("🛑 接收到关闭信号,开始优雅关闭服务器...") + + // 创建带超时的上下文,给服务器 5 秒时间完成当前正在处理的请求 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() + + // 优雅关闭服务器 if err := srv.Shutdown(ctx); err != nil { - common.Log.Fatal("Server forced to shutdown:", err) + common.Log.Fatal("❌ 服务器强制关闭:", err) } - common.Log.Info("Server exiting!") - + common.Log.Info("✅ 服务器已安全退出!") } diff --git a/middleware/CasbinMiddleware.go b/middleware/CasbinMiddleware.go index 8cc4f20..c514163 100644 --- a/middleware/CasbinMiddleware.go +++ b/middleware/CasbinMiddleware.go @@ -1,3 +1,25 @@ +/* +Package middleware 包含系统的各种中间件 + +Casbin 权限控制中间件实现了基于 RBAC(Role-Based Access Control)的权限访问控制模型。 +该中间件负责: +- 验证用户登录状态和账户状态 +- 获取用户的角色信息 +- 基于角色检查用户对特定资源的访问权限 +- 阻止未授权的请求访问受保护的资源 + +权限控制流程: +1. 从 JWT Token 中获取当前登录用户信息 +2. 检查用户账户状态(是否被禁用) +3. 获取用户的所有有效角色 +4. 根据请求的 URL 和 HTTP 方法进行权限检查 +5. 允许或拒绝请求继续执行 + +RBAC 模型说明: +- Subject(主体):用户的角色关键字 +- Object(对象):请求的 API 路径 +- Action(动作):HTTP 请求方法(GET、POST、PUT、DELETE 等) +*/ package middleware import ( @@ -12,35 +34,63 @@ import ( "github.com/gin-gonic/gin" ) +// checkLock 权限检查互斥锁 +// 确保同一时间只有一个请求在执行权限校验,避免并发访问导致的权限检查异常 var checkLock sync.Mutex -// Casbin中间件, 基于RBAC的权限访问控制模型 +// CasbinMiddleware Casbin 权限控制中间件 +// 基于 RBAC 模型的权限访问控制中间件,用于保护需要权限验证的 API 接口 +// +// 权限验证流程: +// 1. 获取当前登录用户信息 +// 2. 验证用户状态(是否被禁用) +// 3. 收集用户的有效角色 +// 4. 构建权限检查参数(角色、路径、方法) +// 5. 调用 Casbin 引擎进行权限验证 +// 6. 根据验证结果决定是否允许请求继续 func CasbinMiddleware() gin.HandlerFunc { return func(c *gin.Context) { + // ==================== 用户身份验证 ==================== + + // 从 JWT Token 中获取当前登录用户信息 user, err := isql.User.GetCurrentLoginUser(c) if err != nil { tools.Response(c, 401, 401, nil, "用户未登录") c.Abort() return } + + // 检查用户账户状态,禁用用户不允许访问 if user.Status != 1 { tools.Response(c, 401, 401, nil, "当前用户已被禁用") c.Abort() return } - // 获得用户的全部角色 + + // ==================== 角色信息收集 ==================== + + // 获取用户的所有角色 roles := user.Roles - // 获得用户全部未被禁用的角色的Keyword + + // 收集所有有效角色的关键字(只包含启用状态的角色) var subs []string for _, role := range roles { if role.Status == 1 { subs = append(subs, role.Keyword) } } - // 获得请求路径URL + + // ==================== 权限检查参数构建 ==================== + + // 获取请求的 API 路径(移除 URL 前缀) obj := strings.TrimPrefix(c.FullPath(), "/"+config.Conf.System.UrlPathPrefix) - // 获取请求方式 + + // 获取 HTTP 请求方法 act := c.Request.Method + + // ==================== 权限验证 ==================== + + // 调用权限检查函数 isPass := check(subs, obj, act) if !isPass { tools.Response(c, 401, 401, nil, "没有权限") @@ -48,21 +98,45 @@ func CasbinMiddleware() gin.HandlerFunc { return } + // 权限验证通过,继续执行后续中间件和处理函数 c.Next() } } +// check 执行具体的权限检查逻辑 +// 该函数使用 Casbin 引擎验证用户角色是否有权限访问指定资源 +// +// 参数说明: +// - subs: 用户的角色关键字列表 +// - obj: 请求的 API 路径 +// - act: HTTP 请求方法 +// +// 返回值: +// - bool: true 表示有权限,false 表示无权限 +// +// 权限检查策略: +// 1. 遍历用户的所有有效角色 +// 2. 对每个角色调用 Casbin 引擎进行权限验证 +// 3. 只要有一个角色通过验证,就认为用户有权限 +// 4. 所有角色都验证失败,则认为用户无权限 func check(subs []string, obj string, act string) bool { - // 同一时间只允许一个请求执行校验, 否则可能会校验失败 + // 使用互斥锁确保权限检查的线程安全 + // 避免并发请求导致 Casbin 引擎状态异常 checkLock.Lock() defer checkLock.Unlock() + isPass := false + + // 遍历用户的所有角色,进行权限检查 for _, sub := range subs { + // 调用 Casbin 引擎进行权限验证 + // Enforce(subject, object, action) 检查主体是否有权限对对象执行指定动作 pass, _ := common.CasbinEnforcer.Enforce(sub, obj, act) if pass { isPass = true - break + break // 只要有一个角色通过验证,就认为有权限 } } + return isPass } diff --git a/public/common/ldap.go b/public/common/ldap.go index 1aae87e..169363e 100644 --- a/public/common/ldap.go +++ b/public/common/ldap.go @@ -1,3 +1,18 @@ +/* +Package common 提供 LDAP 连接池管理功能 + +LDAP 连接池是系统与 OpenLDAP 服务器交互的核心组件,负责: +- 管理 LDAP 连接的创建、复用和销毁 +- 提供线程安全的连接获取和归还机制 +- 控制最大连接数,避免资源耗尽 +- 支持连接的健康检查和自动重连 + +连接池的设计目标: +1. 提高 LDAP 操作的性能,避免频繁建立连接 +2. 控制并发连接数,保护 LDAP 服务器资源 +3. 提供稳定可靠的连接管理机制 +4. 支持优雅的连接回收和错误处理 +*/ package common import ( @@ -13,133 +28,189 @@ import ( ldap "github.com/go-ldap/ldap/v3" ) -var ldapPool *LdapConnPool -var ldapInit = false -var ldapInitOne sync.Once +// LDAP 连接池相关全局变量 +var ( + ldapPool *LdapConnPool // LDAP 连接池实例 + ldapInit = false // LDAP 初始化标志 + ldapInitOne sync.Once // 确保 LDAP 只初始化一次 +) -// Init 初始化连接 +// InitLDAP 初始化 LDAP 连接池 +// 该函数在系统启动时调用,负责: +// 1. 建立与 LDAP 服务器的初始连接 +// 2. 验证管理员账户凭据 +// 3. 初始化连接池结构 +// 4. 设置连接池参数 func InitLDAP() { + // 防止重复初始化 if ldapInit { return } + // 使用 sync.Once 确保只初始化一次 ldapInitOne.Do(func() { ldapInit = true }) - // Dail有两个参数 network, address, 返回 (*Conn, error) + // ==================== 建立初始 LDAP 连接 ==================== + + // 创建 LDAP 连接,设置 5 秒连接超时 ldapConn, err := ldap.DialURL(config.Conf.Ldap.Url, ldap.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second})) if err != nil { - Log.Panicf("初始化ldap连接异常: %v", err) - panic(fmt.Errorf("初始化ldap连接异常: %v", err)) + Log.Panicf("初始化 LDAP 连接异常: %v", err) + panic(fmt.Errorf("初始化 LDAP 连接异常: %v", err)) } + + // 使用管理员账户绑定 LDAP 连接 err = ldapConn.Bind(config.Conf.Ldap.AdminDN, config.Conf.Ldap.AdminPass) if err != nil { - Log.Panicf("绑定admin账号异常: %v", err) - panic(fmt.Errorf("绑定admin账号异常: %v", err)) + Log.Panicf("绑定 LDAP 管理员账号异常: %v", err) + panic(fmt.Errorf("绑定 LDAP 管理员账号异常: %v", err)) } - // 全局变量赋值 + // ==================== 初始化连接池 ==================== + + // 创建连接池实例 ldapPool = &LdapConnPool{ - conns: make([]*ldap.Conn, 0), - reqConns: make(map[uint64]chan *ldap.Conn), - openConn: 0, - maxOpen: config.Conf.Ldap.MaxConn, + conns: make([]*ldap.Conn, 0), // 可用连接切片 + reqConns: make(map[uint64]chan *ldap.Conn), // 等待连接的请求映射 + openConn: 0, // 当前打开的连接数 + maxOpen: config.Conf.Ldap.MaxConn, // 最大连接数 } + + // 将初始连接放入连接池 PutLADPConn(ldapConn) - // 隐藏密码 + // ==================== 输出初始化信息 ==================== + + // 构建显示用的 DSN(隐藏密码) showDsn := fmt.Sprintf( "%s:******@tcp(%s)", config.Conf.Ldap.AdminDN, config.Conf.Ldap.Url, ) - Log.Info("初始化ldap完成! dsn: ", showDsn) + Log.Info("🔗 LDAP 连接池初始化完成! DSN: ", showDsn) } -// GetLDAPConn 获取 LDAP 连接 +// GetLDAPConn 从连接池获取 LDAP 连接 +// 返回一个可用的 LDAP 连接,使用完毕后需要调用 PutLADPConn 归还 func GetLDAPConn() (*ldap.Conn, error) { return ldapPool.GetConnection() } -// PutLDAPConn 放回 LDAP 连接 +// PutLADPConn 将 LDAP 连接归还到连接池 +// 连接使用完毕后必须调用此方法归还,以便其他请求复用 func PutLADPConn(conn *ldap.Conn) { ldapPool.PutConnection(conn) } +// LdapConnPool LDAP 连接池结构体 +// 实现了线程安全的连接池管理机制 type LdapConnPool struct { - mu sync.Mutex - conns []*ldap.Conn - reqConns map[uint64]chan *ldap.Conn - openConn int - maxOpen int + mu sync.Mutex // 互斥锁,保证线程安全 + conns []*ldap.Conn // 可用连接切片 + reqConns map[uint64]chan *ldap.Conn // 等待连接的请求映射表 + openConn int // 当前打开的连接数 + maxOpen int // 最大允许的连接数 } -// 获取一个 ladp Conn +// GetConnection 从连接池获取一个可用的 LDAP 连接 +// 该方法实现了连接池的核心逻辑: +// 1. 优先从池中获取现有连接 +// 2. 检查连接健康状态 +// 3. 控制最大连接数限制 +// 4. 支持连接等待队列 func (lcp *LdapConnPool) GetConnection() (*ldap.Conn, error) { lcp.mu.Lock() - // 判断当前连接池内是否存在连接 + + // ==================== 从连接池获取现有连接 ==================== + + // 检查连接池中是否有可用连接 connNum := len(lcp.conns) if connNum > 0 { - lcp.openConn++ - conn := lcp.conns[0] - copy(lcp.conns, lcp.conns[1:]) - lcp.conns = lcp.conns[:connNum-1] + lcp.openConn++ // 增加打开连接计数 + conn := lcp.conns[0] // 获取第一个连接 + copy(lcp.conns, lcp.conns[1:]) // 移除已获取的连接 + lcp.conns = lcp.conns[:connNum-1] // 调整切片长度 lcp.mu.Unlock() - // 发现连接已经 close 重新获取连接 + + // 检查连接是否已关闭,如果已关闭则创建新连接 if conn.IsClosing() { return initLDAPConn() } return conn, nil } - // 当现有连接池为空时,并且当前超过最大连接限制 - if lcp.maxOpen != 0 && lcp.openConn > lcp.maxOpen { - // 创建一个等待队列 + // ==================== 连接数限制处理 ==================== + + // 当连接池为空且已达到最大连接数限制时 + if lcp.maxOpen != 0 && lcp.openConn >= lcp.maxOpen { + // 创建等待队列,当有连接归还时会通知等待的请求 req := make(chan *ldap.Conn, 1) - reqKey := lcp.nextRequestKeyLocked() - lcp.reqConns[reqKey] = req + reqKey := lcp.nextRequestKeyLocked() // 生成唯一请求键 + lcp.reqConns[reqKey] = req // 将请求加入等待队列 lcp.mu.Unlock() - // 等待请求归还 + // 阻塞等待连接归还 return <-req, nil } else { + // 未达到连接数限制,创建新连接 lcp.openConn++ lcp.mu.Unlock() return initLDAPConn() } } +// PutConnection 将 LDAP 连接归还到连接池 +// 该方法负责连接的回收和分配: +// 1. 优先满足等待队列中的请求 +// 2. 将健康的连接放回连接池 +// 3. 丢弃已关闭的连接 func (lcp *LdapConnPool) PutConnection(conn *ldap.Conn) { - log.Println("放回了一个 LDAP 连接") + log.Println("🔄 归还一个 LDAP 连接到连接池") lcp.mu.Lock() defer lcp.mu.Unlock() - // 先判断是否存在等待的队列 + // ==================== 优先处理等待队列 ==================== + + // 检查是否有等待连接的请求 if num := len(lcp.reqConns); num > 0 { var req chan *ldap.Conn var reqKey uint64 + + // 获取第一个等待请求 for reqKey, req = range lcp.reqConns { break } + + // 从等待队列中移除该请求 delete(lcp.reqConns, reqKey) + + // 将连接发送给等待的请求 req <- conn return - } else { - lcp.openConn-- - if !conn.IsClosing() { - lcp.conns = append(lcp.conns, conn) - } + } + + // ==================== 连接回收处理 ==================== + + // 没有等待请求时,将连接放回连接池 + lcp.openConn-- // 减少打开连接计数 + + // 只有健康的连接才放回连接池 + if !conn.IsClosing() { + lcp.conns = append(lcp.conns, conn) } } -// 获取下一个请求令牌 +// nextRequestKeyLocked 生成下一个唯一的请求键 +// 用于标识等待队列中的请求,确保每个等待请求都有唯一标识 +// 注意:调用此方法时必须已经持有互斥锁 func (lcp *LdapConnPool) nextRequestKeyLocked() uint64 { for { - reqKey := rand.Uint64() - if _, ok := lcp.reqConns[reqKey]; !ok { + reqKey := rand.Uint64() // 生成随机数作为请求键 + if _, ok := lcp.reqConns[reqKey]; !ok { // 确保键的唯一性 return reqKey } } diff --git a/routes/a_routes.go b/routes/a_routes.go index f81f605..68dd797 100644 --- a/routes/a_routes.go +++ b/routes/a_routes.go @@ -1,3 +1,26 @@ +/* +Package routes 系统路由配置的主入口 + +该文件负责初始化和配置整个系统的路由结构,包括: +- Gin 引擎的基础配置 +- 全局中间件的注册和配置 +- 静态文件服务配置 +- API 路由组的初始化 +- Swagger 文档路由配置 + +路由架构说明: +1. 静态文件路由:服务前端 Vue 应用 +2. API 路由组:所有后端 API 接口 +3. Swagger 文档:API 文档和测试界面 + +中间件执行顺序: +1. 静态文件服务中间件 +2. 限流中间件(令牌桶算法) +3. CORS 跨域中间件 +4. 操作日志中间件 +5. JWT 认证中间件(特定路由) +6. Casbin 权限中间件(特定路由) +*/ package routes import ( @@ -15,19 +38,36 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -// 初始化 +// InitRoutes 初始化系统路由 +// 该函数是整个路由系统的入口点,负责: +// 1. 配置 Gin 引擎的运行模式 +// 2. 注册全局中间件 +// 3. 配置静态文件服务 +// 4. 初始化 API 路由组 +// 5. 配置 Swagger 文档路由 +// +// 返回值: +// - *gin.Engine: 配置完成的 Gin 引擎实例 func InitRoutes() *gin.Engine { - //设置模式 + // ==================== Gin 引擎配置 ==================== + + // 根据配置文件设置 Gin 运行模式(debug/release/test) gin.SetMode(config.Conf.System.Mode) - // 创建带有默认中间件的路由: - // 日志与恢复中间件 + // 创建 Gin 引擎实例,包含默认的日志和恢复中间件 r := gin.Default() - // 创建不带中间件的路由: + + // 备选方案:创建不带默认中间件的引擎 // r := gin.New() // r.Use(gin.Recovery()) + // ==================== 静态文件服务配置 ==================== + + // 配置静态文件服务,用于提供前端 Vue 应用 r.Use(middleware.Serve("/", middleware.EmbedFolder(static.Static, "dist"))) + + // 配置 NoRoute 处理器,用于 SPA 路由支持 + // 当请求的路由不存在时,返回 index.html,让前端路由接管 r.NoRoute(func(c *gin.Context) { data, err := static.Static.ReadFile("dist/index.html") if err != nil { @@ -37,38 +77,54 @@ func InitRoutes() *gin.Engine { c.Data(http.StatusOK, "text/html; charset=utf-8", data) }) - // 启用限流中间件 - // 默认每50毫秒填充一个令牌,最多填充200个 + // ==================== 全局中间件配置 ==================== + + // 配置限流中间件(令牌桶算法) + // 根据配置文件设置令牌填充间隔和桶容量,防止 API 被恶意调用 fillInterval := time.Duration(config.Conf.RateLimit.FillInterval) capacity := config.Conf.RateLimit.Capacity r.Use(middleware.RateLimitMiddleware(time.Millisecond*fillInterval, capacity)) - // 启用全局跨域中间件 + // 启用 CORS 跨域中间件 + // 允许前端应用跨域访问后端 API r.Use(middleware.CORSMiddleware()) // 启用操作日志中间件 + // 记录所有 API 请求的详细信息,用于审计和问题排查 r.Use(middleware.OperationLogMiddleware()) - // 初始化JWT认证中间件 + // ==================== JWT 认证中间件初始化 ==================== + + // 初始化 JWT 认证中间件 authMiddleware, err := middleware.InitAuth() if err != nil { - common.Log.Panicf("初始化JWT中间件失败:%v", err) - panic(fmt.Sprintf("初始化JWT中间件失败:%v", err)) + common.Log.Panicf("初始化 JWT 中间件失败:%v", err) + panic(fmt.Sprintf("初始化 JWT 中间件失败:%v", err)) } - // swag - r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - apiGroup := r.Group("/" + config.Conf.System.UrlPathPrefix) - // swag - // 注册路由 - InitBaseRoutes(apiGroup, authMiddleware) // 注册基础路由, 不需要jwt认证中间件,不需要casbin中间件 - InitUserRoutes(apiGroup, authMiddleware) // 注册用户路由, jwt认证中间件,casbin鉴权中间件 - InitGroupRoutes(apiGroup, authMiddleware) // 注册分组路由, jwt认证中间件,casbin鉴权中间件 - InitRoleRoutes(apiGroup, authMiddleware) // 注册角色路由, jwt认证中间件,casbin鉴权中间件 - InitMenuRoutes(apiGroup, authMiddleware) // 注册菜单路由, jwt认证中间件,casbin鉴权中间件 - InitApiRoutes(apiGroup, authMiddleware) // 注册接口路由, jwt认证中间件,casbin鉴权中间件 - InitOperationLogRoutes(apiGroup, authMiddleware) // 注册操作日志路由, jwt认证中间件,casbin鉴权中间件 - InitFieldRelationRoutes(apiGroup, authMiddleware) // 注册操作日志路由, jwt认证中间件,casbin鉴权中间件 - common.Log.Info("初始化路由完成!") + // ==================== Swagger 文档路由 ==================== + + // 注册 Swagger API 文档路由 + // 提供交互式的 API 文档和测试界面 + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + // ==================== API 路由组注册 ==================== + + // 创建 API 路由组,所有业务接口都在此组下 + apiGroup := r.Group("/" + config.Conf.System.UrlPathPrefix) + + // 注册各个功能模块的路由 + // 注意:路由注册顺序会影响中间件的执行顺序 + + InitBaseRoutes(apiGroup, authMiddleware) // 基础路由(登录、登出等,无需认证) + InitUserRoutes(apiGroup, authMiddleware) // 用户管理路由(需要 JWT + Casbin) + InitGroupRoutes(apiGroup, authMiddleware) // 部门/组管理路由(需要 JWT + Casbin) + InitRoleRoutes(apiGroup, authMiddleware) // 角色管理路由(需要 JWT + Casbin) + InitMenuRoutes(apiGroup, authMiddleware) // 菜单管理路由(需要 JWT + Casbin) + InitApiRoutes(apiGroup, authMiddleware) // 接口管理路由(需要 JWT + Casbin) + InitOperationLogRoutes(apiGroup, authMiddleware) // 操作日志路由(需要 JWT + Casbin) + InitFieldRelationRoutes(apiGroup, authMiddleware) // 字段关系路由(需要 JWT + Casbin) + + common.Log.Info("🚀 系统路由初始化完成!") return r } diff --git a/routes/user_routes.go b/routes/user_routes.go index 558baa6..369a59f 100644 --- a/routes/user_routes.go +++ b/routes/user_routes.go @@ -1,3 +1,17 @@ +/* +Package routes 包含系统的所有路由配置 + +用户路由模块负责定义所有与用户管理相关的 API 接口路由,包括: +- 用户基础管理:增删改查、密码管理、状态管理 +- 用户信息查询:个人信息、用户列表 +- 企业 IM 同步:钉钉、企业微信、飞书用户同步 +- LDAP 同步:OpenLDAP 与数据库之间的用户同步 + +路由安全策略: +- 所有用户管理接口都需要 JWT 认证 +- 所有接口都需要通过 Casbin 权限验证 +- 不同操作需要不同的角色权限 +*/ package routes import ( @@ -8,27 +22,57 @@ import ( "github.com/gin-gonic/gin" ) -// 注册用户路由 +// InitUserRoutes 初始化用户管理相关路由 +// 该函数注册所有与用户管理相关的 API 接口,包括基础的 CRUD 操作和企业 IM 同步功能 +// +// 中间件说明: +// - JWT 认证中间件:验证用户登录状态,确保只有登录用户才能访问 +// - Casbin 权限中间件:基于 RBAC 模型验证用户是否有权限执行特定操作 +// +// 参数: +// - r: Gin 路由组,用于注册路由 +// - authMiddleware: JWT 认证中间件实例 +// +// 返回值: +// - gin.IRoutes: 返回路由接口,支持链式调用 func InitUserRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gin.IRoutes { + // 创建用户路由组 user := r.Group("/user") - // 开启jwt认证中间件 - user.Use(authMiddleware.MiddlewareFunc()) - // 开启casbin鉴权中间件 - user.Use(middleware.CasbinMiddleware()) - { - user.GET("/info", controller.User.GetUserInfo) // 暂时未完成 - user.GET("/list", controller.User.List) // 用户列表 - user.POST("/add", controller.User.Add) // 添加用户 - user.POST("/update", controller.User.Update) // 更新用户 - user.POST("/delete", controller.User.Delete) // 删除用户 - user.POST("/changePwd", controller.User.ChangePwd) // 修改用户密码 - user.POST("/changeUserStatus", controller.User.ChangeUserStatus) // 修改用户状态 - user.POST("/syncDingTalkUsers", controller.User.SyncDingTalkUsers) // 同步钉钉用户到平台 - user.POST("/syncWeComUsers", controller.User.SyncWeComUsers) // 同步企业微信用户到平台 - user.POST("/syncFeiShuUsers", controller.User.SyncFeiShuUsers) // 同步飞书用户到平台 - user.POST("/syncOpenLdapUsers", controller.User.SyncOpenLdapUsers) // 同步Ldap用户到平台 - user.POST("/syncSqlUsers", controller.User.SyncSqlUsers) // 同步Sql用户到Ldap + // ==================== 中间件配置 ==================== + + // 启用 JWT 认证中间件,确保用户已登录 + user.Use(authMiddleware.MiddlewareFunc()) + + // 启用 Casbin 权限控制中间件,进行细粒度权限验证 + user.Use(middleware.CasbinMiddleware()) + + // ==================== 用户基础管理接口 ==================== + { + // 用户信息查询接口 + user.GET("/info", controller.User.GetUserInfo) // 获取当前登录用户详细信息 + user.GET("/list", controller.User.List) // 获取用户列表(支持分页和筛选) + + // 用户管理接口 + user.POST("/add", controller.User.Add) // 创建新用户(同时在 MySQL 和 LDAP 中创建) + user.POST("/update", controller.User.Update) // 更新用户信息(同步更新 MySQL 和 LDAP) + user.POST("/delete", controller.User.Delete) // 删除用户(同时从 MySQL 和 LDAP 中删除) + + // 用户账户管理接口 + user.POST("/changePwd", controller.User.ChangePwd) // 修改用户密码(支持管理员修改和用户自助修改) + user.POST("/changeUserStatus", controller.User.ChangeUserStatus) // 修改用户状态(启用/禁用账户) + + // ==================== 企业 IM 同步接口 ==================== + + // 第三方平台用户同步到本系统 + user.POST("/syncDingTalkUsers", controller.User.SyncDingTalkUsers) // 从钉钉同步用户到平台 + user.POST("/syncWeComUsers", controller.User.SyncWeComUsers) // 从企业微信同步用户到平台 + user.POST("/syncFeiShuUsers", controller.User.SyncFeiShuUsers) // 从飞书同步用户到平台 + + // LDAP 与数据库之间的同步 + user.POST("/syncOpenLdapUsers", controller.User.SyncOpenLdapUsers) // 从 OpenLDAP 同步用户到数据库 + user.POST("/syncSqlUsers", controller.User.SyncSqlUsers) // 从数据库同步用户到 OpenLDAP } + return r }