Files
2026-09-11 01:47:20 +08:00

259 lines
6.9 KiB
Go
Raw Permalink 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 (
"context"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/redismodel"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/routers"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/setting"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/tracing"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/bridges/otelslog"
"go.opentelemetry.io/otel"
"gopkg.in/natefinch/lumberjack.v2"
)
func init() {
if err := setupSetting(); err != nil {
panic(err)
}
// OTel 需在 logger 之前初始化,使 LoggerProvider 就绪后 slog 可桥接到 OTel Logs。
setupOTel()
if err := setupSlogLogger(); err != nil {
panic(err)
}
if err := setupMysqlEngine(); err != nil {
panic(err)
}
if err := setupRedisEngine(); err != nil {
panic(err)
}
}
func main() {
gin.SetMode(global.ServerSetting.RunMode)
// 建管理员表并幂等创建初始超级管理员(依赖 MySQL 已就绪,故在 main 中执行)。
migrateAdminTables()
service.EnsureInitialSuperAdmin(context.Background())
router := routers.NewRouter()
s := &http.Server{
Addr: ":" + global.ServerSetting.HttpPort,
Handler: router,
ReadTimeout: time.Duration(global.ServerSetting.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(global.ServerSetting.WriteTimeout) * time.Second,
// 限制http请求头的大小为1MB
MaxHeaderBytes: 1 << 20,
}
// 后台启动 HTTP 服务;启动失败(如端口占用)通过 channel 反馈主流程,避免进程「假存活」。
serverErr := make(chan error, 1)
go func() {
if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
serverErr <- err
}
}()
slog.Info("server started", "port", global.ServerSetting.HttpPort)
// 等待退出信号或服务启动错误。
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
select {
case <-quit:
slog.Info("shutting down server...")
case err := <-serverErr:
slog.Error("http server start failed", "error", err)
}
// 优雅关闭:HTTP 与 OTel 使用独立 context,避免 HTTP 等待耗尽 OTel flush 的超时。
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := s.Shutdown(shutdownCtx); err != nil {
slog.Error("http server shutdown error", "error", err)
}
if global.OTelShutdown != nil {
otelCtx, otelCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer otelCancel()
if err := global.OTelShutdown(otelCtx); err != nil {
slog.Error("otel shutdown error", "error", err)
}
}
slog.Info("server exited")
}
// migrateAdminTables 仅迁移管理后台自建表(admin_users)。
func migrateAdminTables() {
if err := mysqlmodel.AutoMigrateAdminTables(global.MysqlEngine); err != nil {
slog.Error("auto migrate admin tables failed", "error", err)
panic(err)
}
}
func setupSetting() error {
setting, err := setting.NewSetting()
if err != nil {
return err
}
err = setting.ReadSection("Server", &global.ServerSetting)
if err != nil {
return err
}
err = setting.ReadSection("Log", &global.LogSetting)
if err != nil {
return err
}
err = setting.ReadSection("Mysql", &global.MysqlSetting)
if err != nil {
return err
}
err = setting.ReadSection("Redis", &global.RedisSetting)
if err != nil {
return err
}
err = setting.ReadSection("Otel", &global.OtelSetting)
if err != nil {
return err
}
err = setting.ReadSection("JWT", &global.JWTSetting)
if err != nil {
return err
}
// 校验 JWT 关键配置,防止密钥缺失导致可用空密钥伪造 token。
if global.JWTSetting == nil || global.JWTSetting.SecretKey == "" {
return fmt.Errorf("JWT secret key must not be empty")
}
err = setting.ReadSection("Admin", &global.AdminSetting)
if err != nil {
return err
}
err = setting.ReadSection("Quota", &global.QuotaSetting)
if err != nil {
return err
}
err = setting.ReadSection("Cos", &global.CosSetting)
if err != nil {
return err
}
// 校验 Server 配置(时间字段已按秒解析,此处仅做存在性校验)。
if global.ServerSetting == nil {
return fmt.Errorf("Server config section is missing")
}
return nil
}
func setupSlogLogger() error {
// 确保日志目录存在(lumberjack 不会自动创建父目录,目录缺失时文件日志会静默失效)。
if err := os.MkdirAll(global.LogSetting.LogSavePath, 0o755); err != nil {
return fmt.Errorf("create log dir: %w", err)
}
writer := &lumberjack.Logger{
Filename: global.LogSetting.LogSavePath + "/" + global.LogSetting.LogFileName + global.LogSetting.LogFileExt,
MaxSize: global.LogSetting.MaxSize,
MaxBackups: global.LogSetting.MaxBackups,
MaxAge: global.LogSetting.MaxAge,
Compress: true,
}
multiWriter := io.MultiWriter(writer, os.Stdout)
level := slog.LevelInfo
switch global.LogSetting.LogLevel {
case "Debug":
level = slog.LevelDebug
case "Warn":
level = slog.LevelWarn
case "Error":
level = slog.LevelError
default:
level = slog.LevelInfo
}
// 本地 JSON 日志(文件 + stdout),并注入 trace_id/span_id。
localHandler := tracing.OtelContextHandler{
Handler: slog.NewJSONHandler(multiWriter, &slog.HandlerOptions{
AddSource: true,
Level: level,
}),
}
// OTel 日志桥接:将 slog 记录导出到 OTel Logs(LoggerProvider 已在 setupOTel 就绪;
// 未启用 OTel 时为 no-op,不产生开销)。
otelHandler := otelslog.NewHandler("zt-closet-manage-backend")
// 扇出:本地日志 + OTel 日志同时输出。
slog.SetDefault(slog.New(tracing.NewFanoutHandler(localHandler, otelHandler)))
return nil
}
// setupMysqlEngine 初始化 MySQL 数据库连接。
func setupMysqlEngine() error {
var err error
global.MysqlEngine, err = mysqlmodel.NewMysqlEngine(global.MysqlSetting)
if err != nil {
return err
}
return nil
}
// setupRedisEngine 初始化 Redis 连接。
func setupRedisEngine() error {
var err error
global.RedisEngine, err = redismodel.NewRedisEngine(global.RedisSetting)
if err != nil {
return err
}
return nil
}
func setupOTel() {
tracer, shutdown, err := tracing.NewOTelProvider(global.OtelSetting)
// NewOTelProvider 在任何错误路径都会返回可用的 no-op tracer 与安全 shutdown。
global.Tracer = tracer
global.OTelShutdown = shutdown
if err != nil {
// 此时 logger 尚未就绪,用标准输出告警;已降级为 no-op。
fmt.Printf("[WARN] init otel failed, downgraded to no-op: %v\n", err)
}
name := "zt-closet-manage-backend"
if global.OtelSetting != nil && global.OtelSetting.ServiceName != "" {
name = global.OtelSetting.ServiceName
}
global.Meter = otel.Meter(name)
}