All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
主要改动: - 新增交互式环境配置脚本 (scripts/setup-env.sh) - 新增本地启动快捷脚本 (scripts/run-local.sh) - 新增环境变量模板文件 (.env.example) - 部署模式改版:使用嵌入式配置 + 环境变量覆盖 - 添加对象存储功能支持 - 改进 IoT 卡片导入任务 - 优化 OpenAPI 文档生成 - 删除旧的配置文件,改用嵌入式默认配置
9.0 KiB
9.0 KiB
默认超级管理员自动初始化功能
功能概述
系统在 API 服务启动时,自动检查数据库中是否存在超级管理员账号。如果不存在,则自动创建一个默认的超级管理员账号,确保系统首次部署后可以立即登录使用。
业务背景
问题:
- 首次部署新环境(开发、测试、生产)时,数据库中没有任何管理员账号
- 无法登录管理后台,需要手动执行 SQL 或脚本创建管理员
- 人为操作容易出错,不同环境的初始账号可能不一致
解决方案:
- 系统启动时自动检测并创建默认超级管理员
- 支持配置文件自定义账号信息
- 幂等操作,多次启动不会重复创建
技术实现
核心逻辑
1. 初始化时机
// internal/bootstrap/bootstrap.go
func Bootstrap(deps *Dependencies) (*BootstrapResult, error) {
// ... 其他初始化 ...
// 4. 初始化默认超级管理员(降级处理:失败不中断启动)
if err := initDefaultAdmin(deps, services); err != nil {
deps.Logger.Error("初始化默认超级管理员失败", zap.Error(err))
}
// ... 继续初始化 ...
}
初始化顺序:
- Store 层初始化
- GORM Callbacks 注册
- Service 层初始化
- 默认管理员初始化 ← 在此执行
- Middleware 层初始化
- Handler 层初始化
2. 检查逻辑
// 检查是否已存在超级管理员(user_type = 1)
var count int64
err := db.Model(&Account{}).Where("user_type = ?", constants.UserTypeSuperAdmin).Count(&count).Error
if count > 0 {
// 已存在,跳过创建
logger.Info("超级管理员账号已存在,跳过初始化", zap.Int64("count", count))
return nil
}
// count == 0,创建默认管理员
确保唯一性:
- ✅ 检查条件:
user_type = 1(精确匹配超级管理员类型) - ✅ 创建条件:只有
count == 0时才创建 - ✅ 幂等性:多次启动不会重复创建
- ✅ 并发安全:启动时单进程执行,无并发问题
3. 账号信息来源
优先级:配置文件 > 代码默认值
// 代码默认值(pkg/constants/constants.go)
const (
DefaultAdminUsername = "admin"
DefaultAdminPassword = "Admin@123456"
DefaultAdminPhone = "13800000000"
)
// 读取配置文件(可选)
username := constants.DefaultAdminUsername
password := constants.DefaultAdminPassword
phone := constants.DefaultAdminPhone
if cfg.DefaultAdmin.Username != "" {
username = cfg.DefaultAdmin.Username // 使用配置文件值
}
if cfg.DefaultAdmin.Password != "" {
password = cfg.DefaultAdmin.Password
}
if cfg.DefaultAdmin.Phone != "" {
phone = cfg.DefaultAdmin.Phone
}
4. 创建账号
account := &model.Account{
Username: username,
Phone: phone,
Password: password, // 原始密码,CreateSystemAccount 内部会进行 bcrypt 哈希
UserType: constants.UserTypeSuperAdmin, // 1=超级管理员
Status: constants.StatusEnabled, // 1=启用
}
// 调用 Service 层的系统创建方法(绕过当前用户检查)
err := services.Account.CreateSystemAccount(ctx, account)
CreateSystemAccount 方法特点:
- 绕过
currentUserID检查(因为是系统初始化,没有当前用户) - 仍然保留用户名和手机号唯一性检查
- 密码自动进行 bcrypt 哈希
- 跳过数据权限过滤(使用
SkipDataPermission(ctx))
降级处理
if err := initDefaultAdmin(deps, services); err != nil {
deps.Logger.Error("初始化默认超级管理员失败", zap.Error(err))
// 不返回错误,继续启动服务
}
设计理由:
- 默认管理员创建失败不应导致整个服务无法启动
- 可能的失败原因:数据库连接问题、用户名/手机号冲突等
- 管理员可以通过日志查看失败原因,手动处理
配置说明
使用代码默认值(无需配置)
直接启动服务,系统使用内置默认值:
- 用户名:
admin - 密码:
Admin@123456 - 手机号:
13800000000
自定义配置
通过环境变量自定义:
export JUNHONG_DEFAULT_ADMIN_USERNAME="自定义用户名"
export JUNHONG_DEFAULT_ADMIN_PASSWORD="自定义密码"
export JUNHONG_DEFAULT_ADMIN_PHONE="自定义手机号"
注意:
- 配置项为可选,不参与
ValidateRequired()验证 - 任何字段留空则使用代码默认值
- 密码必须足够复杂(建议包含大小写字母、数字、特殊字符)
使用示例
场景1:首次部署(空数据库)
# 启动服务
go run cmd/api/main.go
日志输出:
{"level":"info","msg":"默认超级管理员创建成功","username":"admin","phone":"13800000000"}
结果:
- 数据库
tb_account表新增一条记录 user_type = 1(超级管理员)username = "admin"phone = "13800000000"password为 bcrypt 哈希值
场景2:已有管理员(再次启动)
# 再次启动服务
go run cmd/api/main.go
日志输出:
{"level":"info","msg":"超级管理员账号已存在,跳过初始化","count":1}
结果:
- 跳过创建,不修改现有数据
场景3:使用自定义配置
设置环境变量:
export JUNHONG_DEFAULT_ADMIN_USERNAME="myadmin"
export JUNHONG_DEFAULT_ADMIN_PASSWORD="MySecurePass@2024"
export JUNHONG_DEFAULT_ADMIN_PHONE="13900000000"
启动服务:
go run cmd/api/main.go
日志输出:
{"level":"info","msg":"默认超级管理员创建成功","username":"myadmin","phone":"13900000000"}
安全注意事项
1. 密码安全
- ✅ 密码使用 bcrypt 哈希存储,不可逆
- ⚠️ 默认密码相对简单,建议首次登录后立即修改
- ⚠️ 生产环境建议通过配置文件使用更复杂的密码
2. 权限控制
- ✅ 超级管理员拥有所有权限,跳过数据权限过滤
- ⚠️ 不要将超级管理员账号用于日常操作
- ⚠️ 建议为日常管理员创建独立的平台用户账号
3. 审计日志
- ✅ 创建成功/跳过都记录在
logs/app.log - ✅ 包含时间戳、用户名、手机号
- ⚠️ 日志中不会记录明文密码
4. 配置安全
- ✅ 配置通过环境变量设置,不存储在代码仓库中
- ⚠️ 确保环境变量安全(使用密钥管理服务或加密存储)
- ⚠️ 生产环境务必修改默认密码
手动创建管理员(备用方案)
如果自动初始化失败,可以手动执行以下 SQL:
-- 生成 bcrypt 哈希密码(使用 Go 代码或在线工具)
-- bcrypt.GenerateFromPassword([]byte("Admin@123456"), bcrypt.DefaultCost)
-- 示例哈希值(实际使用时需重新生成):
-- $2a$10$abcdefghijklmnopqrstuvwxyz...
INSERT INTO tb_account (
username,
phone,
password,
user_type,
status,
created_at,
updated_at
) VALUES (
'admin',
'13800000000',
'$2a$10$...your-bcrypt-hash...', -- 替换为实际的 bcrypt 哈希
1, -- 超级管理员
1, -- 启用
NOW(),
NOW()
);
生成 bcrypt 哈希工具:
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
password := "Admin@123456"
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
fmt.Println(string(hash))
}
相关文件
pkg/constants/constants.go- 默认值常量定义pkg/config/config.go- 配置结构定义pkg/config/defaults/config.yaml- 嵌入式默认配置docs/environment-variables.md- 环境变量配置文档internal/service/account/service.go- CreateSystemAccount 方法internal/bootstrap/admin.go- initDefaultAdmin 函数internal/bootstrap/bootstrap.go- Bootstrap 主流程
常见问题
Q1: 为什么要用系统创建方法而不是直接插入数据库?
A: 保持业务逻辑统一性和数据一致性:
- 复用用户名/手机号唯一性检查逻辑
- 自动进行密码哈希处理
- 遵循分层架构原则(通过 Service 层操作)
- 未来扩展更容易(如添加审计日志、事件通知等)
Q2: 如果数据库有多个超级管理员怎么办?
A: 系统检查 user_type = 1 的账号数量:
count == 0:创建默认管理员count > 0:跳过创建(不管有几个)- 系统不会删除或修改现有的超级管理员
Q3: 可以通过配置禁用这个功能吗?
A: 目前不支持配置禁用,因为这是核心初始化逻辑。如果需要禁用:
- 手动创建一个超级管理员账号(系统会自动跳过)
- 或者修改代码注释掉
initDefaultAdmin()调用
Q4: 创建失败会影响服务启动吗?
A: 不会。采用降级处理策略:
- 创建失败只记录错误日志
- 服务继续正常启动
- 管理员可通过日志排查原因并手动创建
Q5: 如何验证管理员创建成功?
A: 三种方式:
- 查看
logs/app.log,搜索 "默认超级管理员创建成功" - 查询数据库:
SELECT * FROM tb_account WHERE user_type = 1; - 尝试使用默认账号登录管理后台