初始化
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// API 进程入口,负责装配依赖、启动 HTTP 服务和异步任务处理器并执行平滑关闭。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"juhe-factory/api/internal/cache"
|
||||
"juhe-factory/api/internal/config"
|
||||
"juhe-factory/api/internal/database"
|
||||
"juhe-factory/api/internal/datetime"
|
||||
"juhe-factory/api/internal/handler"
|
||||
adminmodule "juhe-factory/api/internal/modules/admin"
|
||||
"juhe-factory/api/internal/modules/productimage"
|
||||
"juhe-factory/api/internal/modules/prompt"
|
||||
"juhe-factory/api/internal/queue"
|
||||
"juhe-factory/api/internal/security"
|
||||
"juhe-factory/api/internal/server"
|
||||
"juhe-factory/api/internal/service"
|
||||
"juhe-factory/api/internal/storage"
|
||||
"juhe-factory/api/internal/worker"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
time.Local = datetime.Location
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
slog.Error("加载配置失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
gormDB, sqlDB, err := database.Open(cfg)
|
||||
if err != nil {
|
||||
slog.Error("连接 PostgreSQL 失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
|
||||
redisClient, err := cache.Open(cfg)
|
||||
if err != nil {
|
||||
slog.Error("连接 Redis 失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer redisClient.Close()
|
||||
|
||||
queueClient := queue.NewClient(cfg)
|
||||
defer queueClient.Close()
|
||||
|
||||
healthHandler := handler.NewHealth(sqlDB, redisClient)
|
||||
passwords := security.PasswordHasher{Time: cfg.Argon2Time, Memory: cfg.Argon2Memory, Parallelism: cfg.Argon2Parallelism, HashLength: cfg.Argon2HashLength, SaltLength: cfg.Argon2SaltLength}
|
||||
tokens := security.TokenService{Secret: []byte(cfg.JWTSecretKey), AccessTTL: time.Duration(cfg.JWTAccessMinutes) * time.Minute, RefreshTTL: time.Duration(cfg.JWTRefreshHours) * time.Hour}
|
||||
encryptor, err := security.NewEncryptor(cfg.EncryptionKey)
|
||||
if err != nil {
|
||||
slog.Error("加载敏感配置加密密钥失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
authService := service.NewAuth(gormDB, passwords, tokens)
|
||||
if err := authService.Bootstrap(cfg.BootstrapAdminUsername, cfg.BootstrapAdminPassword); err != nil {
|
||||
slog.Error("初始化管理员失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cos, err := storage.NewCOS(context.Background(), cfg)
|
||||
if err != nil {
|
||||
slog.Error("初始化 COS 失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
adminDataService := &service.AdminData{DB: gormDB, Passwords: passwords, Encryptor: encryptor, HTTPClient: http.DefaultClient, EncryptionKeyVersion: cfg.EncryptionKeyVersion}
|
||||
webService := &service.Web{DB: gormDB, Passwords: passwords, Tokens: security.WebTokenService{Secret: []byte(cfg.JWTSecretKey), AccessTTL: time.Duration(cfg.JWTAccessMinutes) * time.Minute}, RefreshTTL: time.Duration(cfg.JWTRefreshHours) * time.Hour}
|
||||
creativeService := &service.Creative{DB: gormDB, Queue: queueClient, Encryptor: encryptor, HTTPClient: http.DefaultClient}
|
||||
adminService := adminmodule.NewService(gormDB)
|
||||
promptService := prompt.NewService(gormDB)
|
||||
productImageService := productimage.NewService(gormDB, queueClient)
|
||||
creativeHandler := handler.NewCreative(creativeService, promptService, cos)
|
||||
httpServer := server.New(cfg, healthHandler, handler.NewAdminAuth(authService), handler.NewAdminData(adminDataService, adminService, promptService, cos), handler.NewWeb(webService, cos), creativeHandler, handler.NewProductImage(productImageService, cos))
|
||||
|
||||
aiHTTP := worker.NewHTTPClient(cfg.AIHTTPMaxConnections)
|
||||
generationWorker := worker.NewGeneration(gormDB, queueClient, cos, encryptor, aiHTTP, time.Duration(cfg.AIPollIntervalSeconds)*time.Second)
|
||||
mediaWorker := worker.NewMedia(gormDB, cos, encryptor, aiHTTP, cfg.FFmpegPath, cfg.FFprobePath, cfg.PythonPath, cfg.ASRScriptPath)
|
||||
dramaParseWorker := worker.NewDramaParse(gormDB, queueClient, encryptor, aiHTTP)
|
||||
scriptAnalysisWorker := worker.NewScriptAnalysis(gormDB, queueClient, encryptor, aiHTTP)
|
||||
workerMux := asynq.NewServeMux()
|
||||
generationWorker.Register(workerMux)
|
||||
mediaWorker.Register(workerMux)
|
||||
dramaParseWorker.Register(workerMux)
|
||||
scriptAnalysisWorker.Register(workerMux)
|
||||
queueServer := queue.NewServer(cfg)
|
||||
workerErr := make(chan error, 1)
|
||||
go func() {
|
||||
slog.Info("AI 任务 Worker 已启动", "concurrency", cfg.AIWorkerConcurrency)
|
||||
workerErr <- queueServer.Run(workerMux)
|
||||
}()
|
||||
if err := generationWorker.Recover(context.Background()); err != nil {
|
||||
slog.Error("恢复 AI 任务失败", "error", err)
|
||||
}
|
||||
if err := dramaParseWorker.Recover(context.Background()); err != nil {
|
||||
slog.Error("恢复短剧创作解析任务失败", "error", err)
|
||||
}
|
||||
if err := scriptAnalysisWorker.Recover(context.Background()); err != nil {
|
||||
slog.Error("恢复剧本分析任务失败", "error", err)
|
||||
}
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
slog.Info("API 服务已启动", "address", cfg.ServerAddress)
|
||||
serverErr <- httpServer.ListenAndServe()
|
||||
}()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case sig := <-stop:
|
||||
slog.Info("收到停止信号", "signal", sig.String())
|
||||
case err = <-serverErr:
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
slog.Error("API 服务异常退出", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case err = <-workerErr:
|
||||
if err != nil {
|
||||
slog.Error("AI 任务 Worker 异常退出", "error", err)
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
queueServer.Shutdown()
|
||||
if err := httpServer.Shutdown(ctx); err != nil {
|
||||
slog.Error("API 服务关闭失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user