This commit is contained in:
Guwan 2025-08-26 00:43:50 +08:00
parent 06b6c87148
commit 5c82714fd1
9 changed files with 915 additions and 292 deletions

145
COMMENTS_SUMMARY.md Normal file
View File

@ -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 项目的代码质量和可维护性得到了显著提升。

View File

@ -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 基础 DNdc=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 DNou=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"` // 是否同步更新已同步的用户信息
}

View File

@ -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) {

View File

@ -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("邮箱已存在,请勿重复添加"))
}

111
main.go
View File

@ -1,3 +1,25 @@
/*
Package main Go LDAP Admin 项目的主入口文件
Go LDAP Admin 是一个基于 Go + Vue 实现的 OpenLDAP 后台管理系统
旨在为 OpenLDAP 服务端提供一个简单易用清晰美观的现代化管理后台
主要功能
- 用户管理支持用户的增删改查密码管理等
- 组织架构管理支持部门/组的层级管理
- 权限控制基于 RBAC 的角色权限管理
- LDAP 集成 OpenLDAP 服务器双向同步
- 企业 IM 集成支持钉钉企业微信飞书等平台同步
- 操作审计完整的操作日志记录
技术栈
- Web 框架Gin
- ORMGORM
- 数据库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("✅ 服务器已安全退出!")
}

View File

@ -1,3 +1,25 @@
/*
Package middleware 包含系统的各种中间件
Casbin 权限控制中间件实现了基于 RBACRole-Based Access Control的权限访问控制模型
该中间件负责
- 验证用户登录状态和账户状态
- 获取用户的角色信息
- 基于角色检查用户对特定资源的访问权限
- 阻止未授权的请求访问受保护的资源
权限控制流程
1. JWT Token 中获取当前登录用户信息
2. 检查用户账户状态是否被禁用
3. 获取用户的所有有效角色
4. 根据请求的 URL HTTP 方法进行权限检查
5. 允许或拒绝请求继续执行
RBAC 模型说明
- Subject主体用户的角色关键字
- Object对象请求的 API 路径
- Action动作HTTP 请求方法GETPOSTPUTDELETE
*/
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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}