234 lines
7.1 KiB
Go
234 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/global"
|
|
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/internal/boot"
|
|
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/internal/mysqlmodel"
|
|
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/internal/routers"
|
|
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/pkg/cos"
|
|
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/pkg/setting"
|
|
"github.com/gin-gonic/gin"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
)
|
|
|
|
func init() {
|
|
if err := setupSetting(); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if err := setupSlogLogger(); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if err := setupMysqlEngine(); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
setupCosClient()
|
|
|
|
boot.InitWS() // 初始化 WebSocket 模块:房间管理器、匹配器与消息路由
|
|
}
|
|
|
|
func main() {
|
|
gin.SetMode(global.ServerSetting.RunMode)
|
|
|
|
boot.Start(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)
|
|
}
|
|
|
|
// 优雅关闭:等待在途请求处理完成,超时 5 秒强制退出。
|
|
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)
|
|
}
|
|
|
|
slog.Info("server exited")
|
|
}
|
|
|
|
// setupSetting 加载 configs/config.yaml 各配置段到 global,并完成必填校验与默认值兜底。
|
|
//
|
|
// 注意:Redis 段只读取不建连接(本轮不接入缓存),保留读取是为了配置结构完整、
|
|
// 后续启用缓存时无需改动此处顺序。
|
|
func setupSetting() error {
|
|
s, err := setting.NewSetting()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.ReadSection("Server", &global.ServerSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := s.ReadSection("Log", &global.LogSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := s.ReadSection("Mysql", &global.MysqlSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := s.ReadSection("Redis", &global.RedisSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := s.ReadSection("JWT", &global.JWTSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := s.ReadSection("Upload", &global.UploadSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := s.ReadSection("Cos", &global.CosSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := s.ReadSection("WechatMini", &global.WechatMiniSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := s.ReadSection("WS", &global.WSSetting); err != nil { // 读取 WS 配置段到全局变量
|
|
return err // 读取或反序列化失败直接返回,启动阶段 panic 暴露问题
|
|
}
|
|
|
|
// ===== 必填校验:缺失即启动失败,避免运行期才发现配置错误 =====
|
|
if global.ServerSetting == nil {
|
|
return errors.New("server config section is missing")
|
|
}
|
|
if global.LogSetting == nil {
|
|
return errors.New("log config section is missing")
|
|
}
|
|
if global.MysqlSetting == nil || global.MysqlSetting.DBName == "" {
|
|
return errors.New("mysql config section is missing or db name is empty")
|
|
}
|
|
if global.JWTSetting == nil || global.JWTSetting.SecretKey == "" {
|
|
return errors.New("jwt secret key must not be empty")
|
|
}
|
|
if global.JWTSetting.AccessTokenExpire <= 0 || global.JWTSetting.RefreshTokenExpire <= 0 {
|
|
return errors.New("jwt token expire must be positive")
|
|
}
|
|
|
|
// ===== 默认值兜底:可选段缺失时给安全默认,避免下游 nil 解引用 =====
|
|
if global.UploadSetting == nil {
|
|
global.UploadSetting = &setting.UploadSettingS{}
|
|
}
|
|
if global.UploadSetting.MaxSizeMB <= 0 {
|
|
global.UploadSetting.MaxSizeMB = 20
|
|
}
|
|
if global.CosSetting == nil {
|
|
global.CosSetting = &setting.CosSettingS{}
|
|
}
|
|
if global.WechatMiniSetting == nil {
|
|
global.WechatMiniSetting = &setting.WechatMiniSettingS{}
|
|
}
|
|
// 其余 WS 默认值(心跳/超时/限流/队列)都在 internal/wsgateway 内部兜底,这里只避免 nil 解引用。
|
|
if global.WSSetting == nil { // 配置文件里没有 WS 段
|
|
global.WSSetting = &setting.WSSettingS{} // 赋一个零值结构体,保证各处读取不 panic
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupSlogLogger 初始化全局日志:lumberjack 按大小/天数轮转写文件,同时输出到 stdout,
|
|
// 采用 JSON 结构化格式并按配置级别过滤。
|
|
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,
|
|
LocalTime: true, // 轮转文件名时间戳使用本地时区(默认 UTC,与业务展示时区不一致)
|
|
}
|
|
|
|
multiWriter := io.MultiWriter(writer, os.Stdout)
|
|
|
|
// 级别匹配忽略大小写与首尾空白,避免 "debug"/"Info " 等误配静默回落 Info。
|
|
level := slog.LevelInfo
|
|
unknownLevel := ""
|
|
switch strings.ToLower(strings.TrimSpace(global.LogSetting.LogLevel)) {
|
|
case "debug":
|
|
level = slog.LevelDebug
|
|
case "", "info":
|
|
level = slog.LevelInfo
|
|
case "warn", "warning":
|
|
level = slog.LevelWarn
|
|
case "error":
|
|
level = slog.LevelError
|
|
default:
|
|
unknownLevel = global.LogSetting.LogLevel
|
|
}
|
|
|
|
slog.SetDefault(slog.New(slog.NewJSONHandler(multiWriter, &slog.HandlerOptions{
|
|
AddSource: true,
|
|
Level: level,
|
|
})))
|
|
|
|
if unknownLevel != "" {
|
|
slog.Warn("unknown log level in config, fallback to info", "configured", unknownLevel)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupMysqlEngine 初始化 MySQL 数据库连接。
|
|
func setupMysqlEngine() error {
|
|
var err error
|
|
global.MysqlEngine, err = mysqlmodel.NewMysqlEngine(global.MysqlSetting)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setupCosClient 初始化腾讯云 COS 客户端(头像上传依赖)。
|
|
// 初始化失败仅告警,不阻断服务启动(上传时返回明确的业务错误)。
|
|
func setupCosClient() {
|
|
client, err := cos.NewCosClient()
|
|
if err != nil {
|
|
slog.Warn("init cos client failed, upload features may fail", "error", err)
|
|
return
|
|
}
|
|
global.CosClient = client
|
|
}
|