feat: add UMS CLI for binary packaging and system initialization

- Add Cobra-based CLI with ums init, ums serve, ums version commands
- ums init supports interactive prompts and non-interactive flags
- Generates secure JWT secrets and config.yaml automatically
- Extract server.Serve() function for reuse
- Add cross-platform build targets to Makefile
- Update README with CLI installation and usage instructions

New files:
- cmd/ums/main.go - CLI entry point
- cmd/ums/cmd/root.go - Root command
- cmd/ums/cmd/init.go - Interactive/non-interactive init
- cmd/ums/cmd/serve.go - Server command
- cmd/ums/cmd/version.go - Version command
- internal/server/server.go - Extracted Serve function
This commit is contained in:
2026-04-19 08:59:00 +08:00
parent 7b047e2f11
commit 8d9f157eb8
11 changed files with 1035 additions and 300 deletions

View File

@@ -1,28 +1,10 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/api/handler"
"github.com/user-management-system/internal/api/middleware"
"github.com/user-management-system/internal/api/router"
"github.com/user-management-system/internal/auth"
"github.com/user-management-system/internal/cache"
"github.com/user-management-system/internal/config"
"github.com/user-management-system/internal/database"
"github.com/user-management-system/internal/monitoring"
"github.com/user-management-system/internal/repository"
"github.com/user-management-system/internal/security"
"github.com/user-management-system/internal/service"
"github.com/user-management-system/internal/server"
)
func main() {
@@ -32,240 +14,8 @@ func main() {
log.Fatalf("load config failed: %v", err)
}
// 设置 Gin 模式
gin.SetMode(resolveGinMode(cfg.Server.Mode))
// 初始化数据库
db, err := database.NewDB(cfg)
if err != nil {
log.Fatalf("connect database failed: %v", err)
}
// 执行数据库迁移
if err := db.AutoMigrate(cfg); err != nil {
log.Fatalf("auto migrate failed: %v", err)
}
// P1-3Argon2id 启动时自适应校准
// 在当前机器上测量哈希耗时,超出 500ms 预算则自动降低参数,确保登录接口 P99 < 1000ms。
// 此操作仅在启动阶段执行一次,耗时约 1-3s正常情况下与默认参数一致则跳过
auth.CalibrateArgon2id(500 * time.Millisecond)
// 初始化 JWT 管理器
jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{
HS256Secret: cfg.JWT.Secret,
AccessTokenExpire: time.Duration(cfg.JWT.AccessTokenExpireMinutes) * time.Minute,
RefreshTokenExpire: time.Duration(cfg.JWT.RefreshTokenExpireDays) * 24 * time.Hour,
})
if err != nil {
log.Fatalf("create jwt manager failed: %v", err)
}
// 初始化缓存
// Redis 智能探测:有 Redis 则启用 L2 分布式缓存,无 Redis 则降级到纯 L1 内存缓存。
// 两种模式下系统功能完全等价,区别仅在于多实例场景的缓存共享能力。
// 如需禁用 Redis 探测(即使 Redis 可达也不启用),可将配置中 redis.host 留空。
l1Cache := cache.NewL1Cache()
redisAddr := fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port)
redisEnabled := cfg.Redis.Host != "" && cache.ProbeRedis(redisAddr, cfg.Redis.Password, cfg.Redis.DB)
if !redisEnabled {
log.Printf("cache: running in memory-only mode (Redis unreachable or not configured)")
}
l2Cache := cache.NewRedisCacheWithConfig(cache.RedisCacheConfig{
Enabled: redisEnabled,
Addr: redisAddr,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
})
defer l2Cache.Close()
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
// 初始化 Repository
userRepo := repository.NewUserRepository(db.DB)
roleRepo := repository.NewRoleRepository(db.DB)
permissionRepo := repository.NewPermissionRepository(db.DB)
userRoleRepo := repository.NewUserRoleRepository(db.DB)
rolePermissionRepo := repository.NewRolePermissionRepository(db.DB)
deviceRepo := repository.NewDeviceRepository(db.DB)
loginLogRepo := repository.NewLoginLogRepository(db.DB)
operationLogRepo := repository.NewOperationLogRepository(db.DB)
customFieldRepo := repository.NewCustomFieldRepository(db.DB)
userCustomFieldValueRepo := repository.NewUserCustomFieldValueRepository(db.DB)
themeRepo := repository.NewThemeConfigRepository(db.DB)
socialRepo, err := repository.NewSocialAccountRepository(db.DB)
if err != nil {
log.Fatalf("initialize social account repository failed: %v", err)
}
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
// 初始化 Service
deviceService := service.NewDeviceService(deviceRepo, userRepo)
authService := service.NewAuthService(
userRepo,
socialRepo,
jwtManager,
cacheManager,
8, // passwordMinLength
5, // maxLoginAttempts
15*time.Minute, // loginLockDuration
)
authService.SetRoleRepositories(userRoleRepo, roleRepo)
authService.SetLoginLogRepository(loginLogRepo)
authService.SetDeviceService(deviceService)
// IP 过滤中间件
var ipFilterMiddleware *middleware.IPFilterMiddleware
ipFilter := security.NewIPFilter()
if ipFilter != nil {
ipFilterMiddleware = middleware.NewIPFilterMiddleware(ipFilter, middleware.IPFilterConfig{
TrustProxy: cfg.CORS.AllowCredentials,
})
}
// 初始化异常检测器并注入
anomalyDetector := security.NewAnomalyDetector(security.DefaultAnomalyConfig, ipFilter)
authService.SetAnomalyDetector(anomalyDetector)
log.Println("anomaly detector initialized")
userService := service.NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo)
roleService := service.NewRoleService(roleRepo, rolePermissionRepo)
permissionService := service.NewPermissionService(permissionRepo)
loginLogService := service.NewLoginLogService(loginLogRepo)
operationLogService := service.NewOperationLogService(operationLogRepo)
captchaService := service.NewCaptchaService(cacheManager)
totpService := service.NewTOTPService(userRepo)
passwordResetConfig := service.DefaultPasswordResetConfig()
passwordResetService := service.NewPasswordResetService(userRepo, cacheManager, passwordResetConfig).
WithPasswordHistoryRepo(passwordHistoryRepo)
webhookService := service.NewWebhookService(db.DB, service.WebhookServiceConfig{
Enabled: false,
})
exportService := service.NewExportService(userRepo, roleRepo)
statsService := service.NewStatsService(userRepo, loginLogRepo)
customFieldService := service.NewCustomFieldService(customFieldRepo, userCustomFieldValueRepo)
themeService := service.NewThemeService(themeRepo)
// 设置 CORS 配置
middleware.SetCORSConfig(cfg.CORS)
// 初始化中间件
rateLimitMiddleware := middleware.NewRateLimitMiddleware(cfg.RateLimit)
authMiddleware := middleware.NewAuthMiddleware(
jwtManager,
userRepo,
userRoleRepo,
l1Cache,
)
authMiddleware.SetCacheManager(cacheManager)
opLogMiddleware := middleware.NewOperationLogMiddleware(operationLogRepo)
// 初始化 Handler
authHandler := handler.NewAuthHandler(authService)
userHandler := handler.NewUserHandler(userService)
roleHandler := handler.NewRoleHandler(roleService)
permissionHandler := handler.NewPermissionHandler(permissionService)
deviceHandler := handler.NewDeviceHandler(deviceService)
logHandler := handler.NewLogHandler(loginLogService, operationLogService)
captchaHandler := handler.NewCaptchaHandler(captchaService)
totpHandler := handler.NewTOTPHandler(authService, totpService)
webhookHandler := handler.NewWebhookHandler(webhookService)
exportHandler := handler.NewExportHandler(exportService)
statsHandler := handler.NewStatsHandler(statsService)
passwordResetHandler := handler.NewPasswordResetHandler(passwordResetService)
smsHandler := handler.NewSMSHandler(authService, nil)
avatarHandler := handler.NewAvatarHandler(userRepo)
customFieldHandler := handler.NewCustomFieldHandler(customFieldService)
themeHandler := handler.NewThemeHandler(themeService)
// 初始化 SSO 管理器
ssoManager := auth.NewSSOManager()
ssoClientsStore := auth.NewDefaultSSOClientsStore()
ssoHandler := handler.NewSSOHandler(ssoManager, ssoClientsStore)
// 系统设置服务
settingsService := service.NewSettingsService()
settingsHandler := handler.NewSettingsHandler(settingsService)
// SSO 会话清理 context随服务器关闭而取消
ssoCtx, ssoCancel := context.WithCancel(context.Background())
defer ssoCancel()
ssoManager.StartCleanup(ssoCtx)
// 初始化监控指标CRIT-01/02 修复:确保指标被初始化并挂载)
metrics := monitoring.GetGlobalMetrics()
sloMetrics := monitoring.GetGlobalSLOMetrics()
// CRIT-03 修复:启动后台 goroutine 定期采集系统指标runtime + DB 连接池)
metricsCtx, metricsCancel := context.WithCancel(context.Background())
defer metricsCancel()
go monitoring.StartSystemMetricsCollector(metricsCtx, metrics, sloMetrics, db.DB)
// 设置路由
r := router.NewRouter(
authHandler, userHandler, roleHandler, permissionHandler, deviceHandler,
logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware,
passwordResetHandler, captchaHandler, totpHandler, webhookHandler,
ipFilterMiddleware, exportHandler, statsHandler, smsHandler, customFieldHandler, themeHandler, ssoHandler,
settingsHandler, metrics, avatarHandler,
)
engine := r.Setup()
// 健康检查(增强版:存活/就绪分离,检查数据库连接)
healthCheck := monitoring.NewHealthCheck(db.DB)
engine.GET("/health", healthCheck.Handler)
engine.GET("/health/live", healthCheck.LivenessHandler)
engine.GET("/health/ready", healthCheck.ReadinessHandler)
// 启动服务器
addr := fmt.Sprintf(":%d", cfg.Server.Port)
srv := &http.Server{
Addr: addr,
Handler: engine,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
go func() {
log.Printf("server listening on %s", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen failed: %v", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down server...")
// 关闭 Webhook 服务,等待投递任务完成
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := webhookService.Shutdown(shutdownCtx); err != nil {
log.Printf("webhook service shutdown: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("server forced to shutdown: %v", err)
}
log.Println("server exited")
}
func resolveGinMode(mode string) string {
switch mode {
case "debug":
return gin.DebugMode
case "test":
return gin.TestMode
default:
return gin.ReleaseMode
if err := server.Serve(cfg); err != nil {
log.Fatalf("server failed: %v", err)
}
}

500
cmd/ums/cmd/init.go Normal file
View File

@@ -0,0 +1,500 @@
package cmd
import (
"bufio"
"crypto/rand"
"encoding/hex"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"golang.org/x/term"
"github.com/user-management-system/internal/config"
"github.com/user-management-system/internal/database"
)
// initCmd is the init subcommand
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize the User Management System",
Long: `Interactively initialize UMS: generate secrets, configure database,
bootstrap admin user, and create configuration files.
This command can run in two modes:
1. Interactive mode (no flags): Prompts for all required values
2. Non-interactive mode (with flags): Uses provided values directly
Examples:
# Interactive mode
ums init
# Non-interactive mode
ums init --admin-user admin --admin-pass MySecret123 --admin-email admin@example.com`,
RunE: func(cmd *cobra.Command, args []string) error {
return runInit(cmd)
},
}
var (
initDBType string
initDBPath string
initRedisEnable bool
initRedisHost string
initRedisPort int
initRedisPassword string
adminUser string
adminPass string
adminEmail string
initPort int
initCorsOrigin string
yesFlag bool
)
func init() {
initCmd.Flags().StringVar(&initDBType, "db-type", "sqlite", "database type (sqlite, postgresql, mysql)")
initCmd.Flags().StringVar(&initDBPath, "db-path", "./data/user_management.db", "database path or connection string")
initCmd.Flags().BoolVar(&initRedisEnable, "redis-enable", false, "enable Redis")
initCmd.Flags().StringVar(&initRedisHost, "redis-host", "localhost", "Redis host")
initCmd.Flags().IntVar(&initRedisPort, "redis-port", 6379, "Redis port")
initCmd.Flags().StringVar(&initRedisPassword, "redis-password", "", "Redis password")
initCmd.Flags().StringVar(&adminUser, "admin-user", "", "admin username")
initCmd.Flags().StringVar(&adminPass, "admin-pass", "", "admin password")
initCmd.Flags().StringVar(&adminEmail, "admin-email", "", "admin email")
initCmd.Flags().IntVar(&initPort, "port", 8080, "server port")
initCmd.Flags().StringVar(&initCorsOrigin, "cors-origin", "http://localhost:3000", "CORS allowed origin")
initCmd.Flags().BoolVar(&yesFlag, "yes", false, "skip confirmation")
rootCmd.AddCommand(initCmd)
}
type initConfig struct {
dbType string
dbPath string
redisEnable bool
redisHost string
redisPort int
redisPassword string
adminUser string
adminPass string
adminEmail string
port int
corsOrigin string
jwtSecret string
bootstrapSecret string
}
func runInit(cmd *cobra.Command) error {
cfg, err := gatherInitConfig()
if err != nil {
return err
}
// 显示配置摘要
fmt.Println("\n=== Configuration Summary ===")
fmt.Printf("Database Type: %s\n", cfg.dbType)
fmt.Printf("Database Path: %s\n", cfg.dbPath)
fmt.Printf("Redis Enabled: %t\n", cfg.redisEnable)
if cfg.redisEnable {
fmt.Printf("Redis Host: %s:%d\n", cfg.redisHost, cfg.redisPort)
}
fmt.Printf("Admin User: %s\n", cfg.adminUser)
fmt.Printf("Admin Email: %s\n", cfg.adminEmail)
fmt.Printf("Server Port: %d\n", cfg.port)
fmt.Printf("CORS Origin: %s\n", cfg.corsOrigin)
fmt.Println("===========================")
if !yesFlag {
fmt.Print("Proceed with initialization? [y/N]: ")
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(strings.ToLower(input))
if input != "y" && input != "yes" {
fmt.Println("Initialization cancelled.")
return nil
}
}
// 1. 创建数据目录
if err := createDataDirectory(cfg); err != nil {
return fmt.Errorf("create data directory failed: %w", err)
}
// 2. 生成密钥
cfg.jwtSecret = generateSecret(32)
cfg.bootstrapSecret = generateSecret(16)
// 3. 创建配置文件
if err := createConfigFiles(cfg); err != nil {
return fmt.Errorf("create config files failed: %w", err)
}
// 4. 执行数据库迁移
if err := runMigrations(cfg); err != nil {
return fmt.Errorf("run migrations failed: %w", err)
}
// 5. 创建管理员账号
if err := createAdminUser(cfg); err != nil {
return fmt.Errorf("create admin user failed: %w", err)
}
fmt.Println("\n=== Initialization Complete ===")
fmt.Println("Configuration files created:")
fmt.Printf(" - %s\n", getConfigPath())
fmt.Printf(" - %s\n", getEnvPath())
fmt.Println("\nTo start the server:")
fmt.Println(" ums serve")
fmt.Println("=============================")
return nil
}
func gatherInitConfig() (*initConfig, error) {
isInteractive := adminUser == "" || adminPass == "" || adminEmail == ""
if isInteractive {
return gatherInteractiveConfig()
}
// 非交互式模式:验证必需参数
if adminUser == "" {
return nil, fmt.Errorf("--admin-user is required")
}
if adminPass == "" {
return nil, fmt.Errorf("--admin-pass is required")
}
if adminEmail == "" {
return nil, fmt.Errorf("--admin-email is required")
}
// 验证密码强度
if len(adminPass) < 8 {
return nil, fmt.Errorf("password must be at least 8 characters")
}
return &initConfig{
dbType: initDBType,
dbPath: initDBPath,
redisEnable: initRedisEnable,
redisHost: initRedisHost,
redisPort: initRedisPort,
redisPassword: initRedisPassword,
adminUser: adminUser,
adminPass: adminPass,
adminEmail: adminEmail,
port: initPort,
corsOrigin: initCorsOrigin,
}, nil
}
func gatherInteractiveConfig() (*initConfig, error) {
reader := bufio.NewReader(os.Stdin)
cfg := &initConfig{
dbType: "sqlite",
dbPath: "./data/user_management.db",
redisEnable: false,
redisHost: "localhost",
redisPort: 6379,
port: 8080,
corsOrigin: "http://localhost:3000",
}
fmt.Println("=== UMS Initialization ===")
// 数据库类型
fmt.Printf("Database type [%s]: ", cfg.dbType)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if input != "" {
cfg.dbType = input
}
// 数据库路径
fmt.Printf("Database path [%s]: ", cfg.dbPath)
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(input)
if input != "" {
cfg.dbPath = input
}
// Redis 启用
fmt.Printf("Enable Redis? [y/N]: ")
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(strings.ToLower(input))
cfg.redisEnable = input == "y" || input == "yes"
if cfg.redisEnable {
fmt.Printf("Redis host [%s]: ", cfg.redisHost)
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(input)
if input != "" {
cfg.redisHost = input
}
fmt.Printf("Redis port [%d]: ", cfg.redisPort)
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(input)
if input != "" {
var port int
fmt.Sscanf(input, "%d", &port)
cfg.redisPort = port
}
fmt.Printf("Redis password (empty for none): ")
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(input)
cfg.redisPassword = input
}
// 管理员信息
fmt.Printf("Admin username [admin]: ")
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(input)
if input != "" {
cfg.adminUser = input
} else {
cfg.adminUser = "admin"
}
fmt.Print("Admin password: ")
password, err := readPassword()
fmt.Println()
if err != nil {
return nil, fmt.Errorf("read password failed: %w", err)
}
if len(password) < 8 {
return nil, fmt.Errorf("password must be at least 8 characters")
}
cfg.adminPass = password
fmt.Printf("Admin email [%s]: ", cfg.adminEmail)
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(input)
if input != "" {
cfg.adminEmail = input
} else if cfg.adminEmail == "" {
cfg.adminEmail = "admin@example.com"
}
// 服务器端口
fmt.Printf("Server port [%d]: ", cfg.port)
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(input)
if input != "" {
var port int
fmt.Sscanf(input, "%d", &port)
cfg.port = port
}
// CORS 域名
fmt.Printf("CORS origin [%s]: ", cfg.corsOrigin)
input, _ = reader.ReadString('\n')
input = strings.TrimSpace(input)
if input != "" {
cfg.corsOrigin = input
}
return cfg, nil
}
func readPassword() (string, error) {
intrFd := int(os.Stdin.Fd())
if term.IsTerminal(intrFd) {
password, err := term.ReadPassword(intrFd)
if err != nil {
return "", err
}
return string(password), nil
}
// 非终端模式,使用普通输入
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
return strings.TrimSpace(input), nil
}
func generateSecret(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
func createDataDirectory(cfg *initConfig) error {
var dir string
if cfg.dbType == "sqlite" {
dir = filepath.Dir(cfg.dbPath)
if dir == "." || dir == "" {
dir = "./data"
}
} else {
dir = "./data"
}
if dir == "" || dir == "." {
return nil
}
return os.MkdirAll(dir, 0755)
}
func getConfigPath() string {
return "./config.yaml"
}
func getEnvPath() string {
return "./.env"
}
func createConfigFiles(cfg *initConfig) error {
// 创建 .env 文件
envContent := fmt.Sprintf(`# Auto-generated by ums init
# DO NOT COMMIT THIS FILE
# JWT Configuration
JWT_SECRET=%s
JWT_REFRESH_SECRET=%s
# Bootstrap secret for admin initialization
BOOTSTRAP_SECRET=%s
# Database
DATABASE_PATH=%s
# Admin credentials
DEFAULT_ADMIN_EMAIL=%s
DEFAULT_ADMIN_PASSWORD=%s
# Redis
REDIS_ENABLED=%t
REDIS_HOST=%s
REDIS_PORT=%d
REDIS_PASSWORD=%s
# CORS
CORS_ALLOWED_ORIGINS=%s
# Server
SERVER_PORT=%d
GIN_MODE=release
`,
cfg.jwtSecret,
cfg.jwtSecret, // refresh secret same as access for simplicity
cfg.bootstrapSecret,
cfg.dbPath,
cfg.adminEmail,
cfg.adminPass,
cfg.redisEnable,
cfg.redisHost,
cfg.redisPort,
cfg.redisPassword,
cfg.corsOrigin,
cfg.port,
)
if err := os.WriteFile(getEnvPath(), []byte(envContent), 0600); err != nil {
return err
}
fmt.Printf("Created: %s\n", getEnvPath())
// 创建 config.yaml
configYAML := fmt.Sprintf(`server:
port: %d
mode: release
host: 0.0.0.0
database:
type: %s
dbname: "%s"
redis:
enabled: %t
host: "%s"
port: %d
password: "%s"
db: 0
jwt:
secret: "%s"
access_token_expire_minutes: 120
refresh_token_expire_days: 7
cors:
allowed_origins:
- "%s"
default:
admin_email: "%s"
admin_password: "%s"
`,
cfg.port,
cfg.dbType,
cfg.dbPath,
cfg.redisEnable,
cfg.redisHost,
cfg.redisPort,
cfg.redisPassword,
cfg.jwtSecret,
cfg.corsOrigin,
cfg.adminEmail,
cfg.adminPass,
)
if err := os.WriteFile(getConfigPath(), []byte(configYAML), 0644); err != nil {
return err
}
fmt.Printf("Created: %s\n", getConfigPath())
return nil
}
func runMigrations(cfg *initConfig) error {
fmt.Println("\nRunning database migrations...")
// 创建一个临时 config 对象用于迁移
tempCfg := &config.Config{
Database: config.DatabaseConfig{
DBName: cfg.dbPath,
},
Default: config.DefaultConfig{
AdminEmail: cfg.adminEmail,
AdminPassword: cfg.adminPass,
},
}
db, err := database.NewDB(tempCfg)
if err != nil {
return fmt.Errorf("connect database failed: %w", err)
}
if err := db.AutoMigrate(tempCfg); err != nil {
return fmt.Errorf("auto migrate failed: %w", err)
}
fmt.Println("Database migrations completed.")
return nil
}
func createAdminUser(cfg *initConfig) error {
fmt.Println("\nCreating admin user...")
// 验证 Redis 连接(如果启用)
if cfg.redisEnable {
addr := net.JoinHostPort(cfg.redisHost, fmt.Sprintf("%d", cfg.redisPort))
conn, err := net.Dial("tcp", addr)
if err != nil {
fmt.Printf("Warning: Redis connection failed: %v\n", err)
fmt.Println("System will run without Redis caching.")
} else {
conn.Close()
fmt.Println("Redis connection verified.")
}
}
// 密码验证已在 gatherConfig 中完成
fmt.Println("Admin user creation skipped (handled by migrations).")
return nil
}

41
cmd/ums/cmd/root.go Normal file
View File

@@ -0,0 +1,41 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var (
cfgFile string
dataDir string
)
// rootCmd is the single instance of the root command
var rootCmd = &cobra.Command{
Use: "ums",
Short: "UMS CLI - User Management System",
Long: `UMS CLI - Command line interface for User Management System
Supported commands:
ums init Initialize the system (interactive or with flags)
ums serve Start the UMS server
ums version Print version information`,
}
func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "./config.yaml", "config file path")
rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "./data", "data directory")
rootCmd.AddCommand(
newVersionCmd(),
initCmd,
serveCmd,
)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

48
cmd/ums/cmd/serve.go Normal file
View File

@@ -0,0 +1,48 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/user-management-system/internal/config"
"github.com/user-management-system/internal/server"
)
var (
servePort string
)
// serveCmd is the serve subcommand
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the UMS server",
Long: `Start the User Management System HTTP server using config.yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
// 设置配置文件路径
viper.SetConfigFile(cfgFile)
// 加载配置
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("load config failed: %w", err)
}
// 允许通过 --port 覆盖配置
if servePort != "" {
var portInt int
if _, err := fmt.Sscanf(servePort, "%d", &portInt); err != nil {
return fmt.Errorf("invalid port: %w", err)
}
cfg.Server.Port = portInt
}
// 启动服务器
return server.Serve(cfg)
},
}
func init() {
serveCmd.Flags().StringVar(&servePort, "port", "", "server port (overrides config)")
rootCmd.AddCommand(serveCmd)
}

31
cmd/ums/cmd/version.go Normal file
View File

@@ -0,0 +1,31 @@
package cmd
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
)
var (
Version = "1.0.0"
Commit = "dev"
BuildDate = "unknown"
)
func newVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print version information",
Long: `Print the UMS version, Go version, and build information`,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("UMS CLI - User Management System\n")
fmt.Printf("Version: %s\n", Version)
fmt.Printf("Commit: %s\n", Commit)
fmt.Printf("Build Date: %s\n", BuildDate)
fmt.Printf("Go Version: %s\n", runtime.Version())
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
return nil
},
}
}

9
cmd/ums/main.go Normal file
View File

@@ -0,0 +1,9 @@
package main
import (
"github.com/user-management-system/cmd/ums/cmd"
)
func main() {
cmd.Execute()
}