This commit is contained in:
toom1996
2026-09-29 19:59:30 +08:00
parent d15d2a4701
commit 361ffc01af
28 changed files with 896 additions and 47 deletions

View File

@ -8,7 +8,7 @@ import (
"context"
"errors"
"flag"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
@ -41,6 +41,19 @@ func s4BaseURL(cfgBase string, up *storage.S4Uploader) string {
return cfgBase
}
// setupLogger 根据运行模式初始化结构化日志(JSON 输出,便于集中采集)。
func setupLogger(mode string) {
level := slog.LevelDebug
switch mode {
case "release":
level = slog.LevelInfo
case "test":
level = slog.LevelWarn
}
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
slog.SetDefault(slog.New(handler))
}
func main() {
configPath := flag.String("config", "", "配置文件路径,默认查找 configs/config.yml")
flag.Parse()
@ -48,35 +61,46 @@ func main() {
// 1. 加载配置(yml 为主,环境变量可覆盖)
cfg, err := config.Load(*configPath)
if err != nil {
log.Fatalf("✗ 加载配置失败: %v", err)
slog.Error("加载配置失败", "error", err)
os.Exit(1)
}
if from := cfg.LoadedFrom(); from != "" {
log.Println("✓ 配置文件:", from)
} else {
log.Println("! 未找到配置文件,使用默认值与环境变量")
// 缺失的必填/推荐敏感配置:本地 TTY 交互提示补填(写回 config.local.yml),
// 容器/CI 等非交互环境缺失必填项则直接报错退出并提示用环境变量注入。
if err := config.PromptMissing(cfg); err != nil {
slog.Error("启动前校验失败", "error", err)
os.Exit(1)
}
gin.SetMode(cfg.Server.Mode)
setupLogger(cfg.Server.Mode)
if from := cfg.LoadedFrom(); from != "" {
slog.Info("配置文件", "path", from)
} else {
slog.Warn("未找到配置文件,使用默认值与环境变量")
}
// 2. 连接数据库
db, err := database.New(cfg.Database)
if err != nil {
log.Fatalf("✗ %v", err)
slog.Error("数据库连接失败", "error", err)
os.Exit(1)
}
defer func() {
if err := database.Close(db); err != nil {
log.Printf("! 关闭数据库连接失败: %v", err)
slog.Error("关闭数据库连接失败", "error", err)
}
}()
// 2.5 结构不再随启动自动迁移:全库「结构 + 索引 + 数据」统一由 cmd/dbtool 导出的
// 纯 SQL 维护(dbtool dump → psql -f)。这里只探活,不执行任何 DDL。
log.Println("✓ PostgreSQL 已连接:", cfg.Database.Addr())
slog.Info("PostgreSQL 已连接", "addr", cfg.Database.Addr())
// 3. 确保上传目录存在(静态文件服务的根目录)
uploadDir, _ := filepath.Abs(cfg.Upload.Dir)
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
log.Fatalf("✗ 创建上传目录失败: %v", err)
slog.Error("创建上传目录失败", "error", err)
os.Exit(1)
}
log.Printf("✓ 静态资源: %s → %s", cfg.Upload.URLPrefix, uploadDir)
slog.Info("静态资源", "prefix", cfg.Upload.URLPrefix, "dir", uploadDir)
// 4.5 初始化公开 ID 混淆(HashID)。
// 对外接口的 id / brand_id 一律用 hashid 编码串,内部仍用数字主键;
@ -137,9 +161,9 @@ func main() {
// free 走公开 CoreIX 样式、VIP 走预签名原图,既方便换域名又能做分级(见 internal/pkg/imgurl)。
imgUp = s4Up
imgDel = s4Up
log.Printf("✓ 图片存储: 缤纷云 S4 bucket=%s base=%s(库只存 key,渲染时拼 base_url)", cfg.S4.Bucket, s4Up.PublicBaseURL())
slog.Info("图片存储(S4)", "bucket", cfg.S4.Bucket, "base", s4Up.PublicBaseURL())
} else {
log.Printf("✓ 图片存储: 本地 %s", cfg.Upload.URLPrefix)
slog.Info("图片存储(本地)", "prefix", cfg.Upload.URLPrefix)
}
// 爬虫入库服务:注入 mediaRepo(引用计数删孤儿)+ imgDel(S4/本地删除器)。
// 必须在 articleSvc / snapSvc 之前创建——图集服务删除时需调用它把「清理存储孤儿图」异步入队。
@ -155,9 +179,9 @@ func main() {
Brand: handler.NewBrandHandler(brandSvc),
StreetSnap: handler.NewStreetSnapHandler(snapSvc, favSvc),
Auth: handler.NewAuthHandler(authSvc),
Favorite: handler.NewFavoriteHandler(favSvc),
History: handler.NewHistoryHandler(histSvc),
Health: handler.NewHealthHandler(),
Favorite: handler.NewFavoriteHandler(favSvc),
History: handler.NewHistoryHandler(histSvc),
Health: handler.NewHealthHandler(),
})
// 5.5 装配管理后台(Backstage)handler,供下方独立引擎使用。
@ -172,9 +196,10 @@ func main() {
}
go func() {
log.Printf("🚀 公开服务已启动: http://localhost:%s", cfg.Server.Port)
slog.Info("公开服务已启动", "addr", "http://localhost:"+cfg.Server.Port)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("✗ 公开服务启动失败: %v", err)
slog.Error("公开服务启动失败", "error", err)
os.Exit(1)
}
}()
@ -191,9 +216,10 @@ func main() {
}
go func() {
log.Printf("🔒 SSG 内部服务已启动: http://localhost:%s (仅构建期/回环可达)", cfg.Server.SSGPort)
slog.Info("SSG 内部服务已启动", "addr", "http://localhost:"+cfg.Server.SSGPort)
if err := srvSSG.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("✗ SSG 内部服务启动失败: %v", err)
slog.Error("SSG 内部服务启动失败", "error", err)
os.Exit(1)
}
}()
@ -216,15 +242,16 @@ func main() {
// 用独立可取消 ctx,优雅关闭时随主流程一起退出。
workerCtx, workerCancel := context.WithCancel(context.Background())
go ingestSvc.Run(workerCtx, 2, 3*time.Second)
log.Printf("⚙ 入库 worker 已启动(队列 ingest_jobs)")
slog.Info("入库 worker 已启动", "queue", "ingest_jobs")
srvBackstage := &http.Server{
Addr: ":" + cfg.Server.BackstagePort,
Handler: backstageEngine,
}
go func() {
log.Printf("🛠 管理后台已启动: http://localhost:%s/admin", cfg.Server.BackstagePort)
slog.Info("管理后台已启动", "addr", "http://localhost:"+cfg.Server.BackstagePort+"/admin")
if err := srvBackstage.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("✗ 管理后台启动失败: %v", err)
slog.Error("管理后台启动失败", "error", err)
os.Exit(1)
}
}()
@ -233,20 +260,20 @@ func main() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("→ 正在关闭服务 ...")
slog.Info("正在关闭服务")
ctx, cancel := context.WithTimeout(context.Background(),
time.Duration(cfg.Server.ShutdownTimeout)*time.Second)
defer cancel()
workerCancel() // 通知入库 worker 停止领取新任务
if err := srv.Shutdown(ctx); err != nil {
log.Printf("! 公开服务优雅关闭超时: %v", err)
slog.Error("公开服务优雅关闭超时", "error", err)
}
if err := srvSSG.Shutdown(ctx); err != nil {
log.Printf("! SSG 内部服务优雅关闭超时: %v", err)
slog.Error("SSG 内部服务优雅关闭超时", "error", err)
}
if err := srvBackstage.Shutdown(ctx); err != nil {
log.Printf("! 管理后台优雅关闭超时: %v", err)
slog.Error("管理后台优雅关闭超时", "error", err)
}
log.Println("✓ 服务已停止")
slog.Info("服务已停止")
}