Files
backend_v2/cmd/server/main.go
toom1996 a6724265ad update
2026-08-26 19:40:16 +08:00

144 lines
4.5 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.

// Command server 启动 Fashion 对外 API 服务。
//
// 本文件是唯一的依赖装配点(组合根):配置 → 数据库 → 仓储 → 服务 → 处理器 → 路由。
// 依赖自下而上显式传入,不使用包级全局变量,便于替换实现与编写测试。
package main
import (
"context"
"errors"
"flag"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"fashionapi/internal/config"
"fashionapi/internal/database"
"fashionapi/internal/handler"
"fashionapi/internal/pkg/hashid"
"fashionapi/internal/pkg/jwt"
"fashionapi/internal/repository"
"fashionapi/internal/router"
"fashionapi/internal/service"
"github.com/gin-gonic/gin"
)
func main() {
configPath := flag.String("config", "", "配置文件路径,默认查找 configs/config.yml")
flag.Parse()
// 1. 加载配置(yml 为主,环境变量可覆盖)
cfg, err := config.Load(*configPath)
if err != nil {
log.Fatalf("✗ 加载配置失败: %v", err)
}
if from := cfg.LoadedFrom(); from != "" {
log.Println("✓ 配置文件:", from)
} else {
log.Println("! 未找到配置文件,使用默认值与环境变量")
}
gin.SetMode(cfg.Server.Mode)
// 2. 连接数据库
db, err := database.New(cfg.Database)
if err != nil {
log.Fatalf("✗ %v", err)
}
defer func() {
if err := database.Close(db); err != nil {
log.Printf("! 关闭数据库连接失败: %v", err)
}
}()
log.Println("✓ MySQL 已连接:", cfg.Database.Addr())
// 3. 确保上传目录存在(静态文件服务的根目录)
uploadDir, _ := filepath.Abs(cfg.Upload.Dir)
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
log.Fatalf("✗ 创建上传目录失败: %v", err)
}
log.Printf("✓ 静态资源: %s → %s", cfg.Upload.URLPrefix, uploadDir)
// 4.5 初始化公开 ID 混淆(HashID)。
// 对外接口的 id / brand_id 一律用 hashid 编码串,内部仍用数字主键;
// 部署可用 HASHID_SECRET 提供独立盐值(默认走内置盐)。
hashid.Init(cfg.Server.HashIDSecret)
// 5. 装配各层依赖
var (
articleRepo = repository.NewArticleRepository(db)
brandRepo = repository.NewBrandRepository(db)
userRepo = repository.NewUserRepository(db)
jwtManager = jwt.NewManager(cfg.JWT.Secret, cfg.JWT.ExpireHours)
articleSvc = service.NewArticleService(articleRepo)
brandSvc = service.NewBrandService(brandRepo)
indexSvc = service.NewIndexService(brandRepo)
authSvc = service.NewAuthService(userRepo, jwtManager)
)
engine := router.New(router.Options{
Config: cfg,
JWT: jwtManager,
Article: handler.NewArticleHandler(articleSvc),
Brand: handler.NewBrandHandler(brandSvc),
Auth: handler.NewAuthHandler(authSvc),
Health: handler.NewHealthHandler(),
})
// 6. 启动对外公开 HTTP 服务(8090),并支持优雅关闭
srv := &http.Server{
Addr: ":" + cfg.Server.Port,
Handler: engine,
}
go func() {
log.Printf("🚀 公开服务已启动: http://localhost:%s", cfg.Server.Port)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("✗ 公开服务启动失败: %v", err)
}
}()
// 7. 启动 SSG 内部服务(默认 8091,仅供 astro build 构建期调用)。
// 该端口由部署层绑定到 127.0.0.1(回环),外部不可达;此处无论如何都启动,
// 即便部署层误绑了 0.0.0.0,SSGToken 中间件也能作为最后一道闸。
ssgEngine := router.NewSSG(router.Options{
Config: cfg,
SSG: handler.NewSSGHandler(brandSvc, indexSvc),
})
srvSSG := &http.Server{
Addr: ":" + cfg.Server.SSGPort,
Handler: ssgEngine,
}
go func() {
log.Printf("🔒 SSG 内部服务已启动: http://localhost:%s (仅构建期/回环可达)", cfg.Server.SSGPort)
if err := srvSSG.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("✗ SSG 内部服务启动失败: %v", err)
}
}()
// 收到中断信号后,给正在处理的请求留出完成时间再退出,
// 避免直接杀进程导致请求被截断、连接池未正常释放。
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("→ 正在关闭服务 ...")
ctx, cancel := context.WithTimeout(context.Background(),
time.Duration(cfg.Server.ShutdownTimeout)*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("! 公开服务优雅关闭超时: %v", err)
}
if err := srvSSG.Shutdown(ctx); err != nil {
log.Printf("! SSG 内部服务优雅关闭超时: %v", err)
}
log.Println("✓ 服务已停止")
}