first commit
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user