Files
usernode/cmd/usernode/app.go
T

145 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"flag"
"fmt"
"log/slog"
"os"
robfigcron "github.com/robfig/cron/v3"
"gorm.io/gorm"
"ws_usernode/internal/api"
"ws_usernode/internal/auth"
"ws_usernode/internal/config"
"ws_usernode/internal/cron"
"ws_usernode/internal/mail"
"ws_usernode/internal/model"
"ws_usernode/internal/router"
"ws_usernode/internal/server"
"ws_usernode/internal/service"
"ws_usernode/internal/system"
)
// bootstrap 加载配置、打开数据库、构建日志。
// cfgPath 为配置文件路径,debug 开启调试日志与 GORM SQL 日志。
func bootstrap(cfgPath string, debug bool) (*config.Config, *slog.Logger, *gorm.DB, error) {
cfg, err := config.Load(cfgPath)
if err != nil {
return nil, nil, nil, err
}
if debug {
cfg.Log.Level = "debug"
}
log := newLogger(cfg.Log.Level, cfg.Log.Format)
db, err := model.Open(cfg.Database.Driver, cfg.Database.DSN, debug)
if err != nil {
return nil, nil, nil, fmt.Errorf("打开数据库: %w", err)
}
return cfg, log, db, nil
}
// commonFlags 定义所有子命令共享的 --config / --debug。
func commonFlags(fs *flag.FlagSet) (cfgPath *string, debug *bool) {
cfgPath = fs.String("config", "config.toml", "配置文件路径(默认 config.toml")
debug = fs.Bool("debug", false, "调试模式(debug 日志 + GORM SQL 日志)")
return cfgPath, debug
}
// newLogger 按配置构建 slog 输出器。
func newLogger(level, format string) *slog.Logger {
var lvl slog.Level
switch level {
case "debug":
lvl = slog.LevelDebug
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
lvl = slog.LevelInfo
}
opts := &slog.HandlerOptions{Level: lvl}
var h slog.Handler
if format == "json" {
h = slog.NewJSONHandler(os.Stderr, opts)
} else {
h = slog.NewTextHandler(os.Stderr, opts)
}
return slog.New(h)
}
// serve 子命令:启动 HTTP 服务(自动迁移 + 定时任务)。
func cmdServe(args []string) error {
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
cfgPath, debug := commonFlags(fs)
if err := fs.Parse(args); err != nil {
return err
}
cfg, log, db, err := bootstrap(*cfgPath, *debug)
if err != nil {
return err
}
sys := system.New(cfg.System, log)
adminSvc := service.NewAdminService(db)
userSvc := service.NewUserService(db, sys, cfg)
keySvc := service.NewKeyService(db, sys, cfg)
auditSvc := service.NewAuditService(db, log)
settingsSvc := service.NewSettingService(db, cfg)
// 认证依赖:DB OTP/会话/重置令牌存储 + 内存图形验证码/登录限速器 + 邮件
captchas := auth.NewMemoryCaptchaStore(cfg.Auth.CaptchaTTL)
otps := auth.NewDBOTPStore(db, auth.DefaultMaxFailures, auth.DefaultFailureWin)
sessions := auth.NewDBSessionStore(db)
resets := auth.NewDBResetTokenStore(db)
limiter := auth.NewRateLimiter(cfg.Auth.MaxLoginFailures, cfg.Auth.LockDuration)
// M3:所有邮件经队列(mail_logs 落库 + 失败重试),OTP 双通道不受邮件故障影响
mailer := mail.NewQueuedMailer(mail.New(cfg.SMTP, log), db, log)
authSvc := service.NewAuthService(db, cfg, otps, captchas, sessions, resets, limiter, mailer, userSvc, adminSvc, auditSvc, log)
approvalSvc := service.NewApprovalService(db, cfg, userSvc, mailer, log)
// M4:设置覆盖默认 TTL(settings 表);生命周期每日维护
userSvc.WithSettings(settingsSvc)
lifecycleSvc := service.NewLifecycleService(db, cfg, sys, userSvc, settingsSvc, mailer, auditSvc, log)
// 启动前自动迁移(骨架阶段保证表结构就绪;M5 部署建议显式 migrate)
if err := model.Migrate(db); err != nil {
return fmt.Errorf("数据库迁移: %w", err)
}
log.Info("db: migrated", "driver", cfg.Database.Driver, "dsn", cfg.Database.DSN)
// 定时任务(M3:邮件重试;M4:过期扫描/回收/审计归档)
sched := robfigcron.New()
cron.New(cfg, mailer, lifecycleSvc, auditSvc, settingsSvc, log).Register(sched)
sched.Start()
defer sched.Stop()
h := api.New(cfg, authSvc, userSvc, keySvc, approvalSvc, auditSvc, settingsSvc)
r := router.New(cfg, h, sessions, log)
srv := server.New(cfg.Server.Listen, r, log)
if err := srv.Run(); err != nil {
return fmt.Errorf("服务异常退出: %w", err)
}
return nil
}
// migrate 子命令:执行数据库迁移。
func cmdMigrate(args []string) error {
fs := flag.NewFlagSet("migrate", flag.ContinueOnError)
cfgPath, debug := commonFlags(fs)
if err := fs.Parse(args); err != nil {
return err
}
cfg, log, db, err := bootstrap(*cfgPath, *debug)
if err != nil {
return err
}
if err := model.Migrate(db); err != nil {
return fmt.Errorf("数据库迁移: %w", err)
}
log.Info("db: migration complete", "driver", cfg.Database.Driver)
return nil
}