package main import ( "context" "embed" "io/fs" "log" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" "gotest/internal/config" "gotest/internal/database" "gotest/internal/handler" "gotest/internal/repository" "gotest/internal/router" "gotest/internal/service" "gotest/internal/sms" "gotest/internal/storage" "gotest/internal/task" "github.com/gin-gonic/gin" ) //go:embed all:web/dist var webDist embed.FS func main() { // 1. 加载配置 cfg, err := config.Load("config.yaml") if err != nil { log.Fatalf("加载配置失败: %v", err) } log.Printf("当前环境: %s", cfg.Env.Name) // 确保运行时目录存在 os.MkdirAll(cfg.Upload.Path, 0755) os.MkdirAll("data", 0755) // 2. 初始化数据库 db, err := database.Init(&cfg.Database) if err != nil { log.Fatalf("初始化数据库失败: %v", err) } // 3. 初始化存储 storageInstance := storage.NewStorage(&cfg.Storage) // 4. 初始化短信服务(预留,按需在业务中使用) smsSender := sms.NewSender(&cfg.SMS) _ = smsSender // 5. 初始化 repository userRepo := repository.NewUserRepository(db.DB) enterpriseRepo := repository.NewEnterpriseRepository(db.DB) shopRepo := repository.NewShopRepository(db.DB) foodRepo := repository.NewFoodRepository(db.DB) gameRepo := repository.NewGameRepository(db.DB) appRepo := repository.NewAppRepository(db.DB) musicRepo := repository.NewMusicRepository(db.DB) // 6. 初始化 service authService := service.NewAuthService(userRepo, cfg) userService := service.NewUserService(userRepo) adminService := service.NewAdminService(userRepo) uploadService := service.NewUploadService(storageInstance, cfg) enterpriseService := service.NewEnterpriseService(enterpriseRepo) shopService := service.NewShopService(shopRepo) foodService := service.NewFoodService(foodRepo) gameService := service.NewGameService(gameRepo) appService := service.NewAppService(appRepo) musicService := service.NewMusicService(musicRepo) // 7. 初始化 handler authHandler := handler.NewAuthHandler(authService) userHandler := handler.NewUserHandler(userService) adminHandler := handler.NewAdminHandler(adminService) uploadHandler := handler.NewUploadHandler(uploadService) enterpriseHandler := handler.NewEnterpriseHandler(enterpriseService) shopHandler := handler.NewShopHandler(shopService) foodHandler := handler.NewFoodHandler(foodService) gameHandler := handler.NewGameHandler(gameService) appHandler := handler.NewAppHandler(appService) musicHandler := handler.NewMusicHandler(musicService) dashboardHandler := handler.NewDashboardHandler(db.DB) // 8. 设置路由 r := router.Setup(cfg, db, authHandler, userHandler, adminHandler, uploadHandler, enterpriseHandler, shopHandler, foodHandler, gameHandler, appHandler, musicHandler, dashboardHandler) // 9. 配置嵌入式前端:18 套网站模板 setupFrontend(r) // 10. 启动定时任务 scheduler := task.New() scheduler.RegisterDefaults() scheduler.Start() // 11. 启动 HTTP 服务(优雅关机) port := cfg.Server.Port if port == 0 { port = 8080 } addr := ":" + strconv.Itoa(port) srv := &http.Server{ Addr: addr, Handler: r, // 生产级超时配置(从 config.yaml 读取,有默认值兜底) ReadTimeout: time.Duration(maxVal(cfg.Server.ReadTimeout, 15)) * time.Second, WriteTimeout: time.Duration(maxVal(cfg.Server.WriteTimeout, 30)) * time.Second, ReadHeaderTimeout: time.Duration(maxVal(cfg.Server.ReadTimeout, 15)) * time.Second, IdleTimeout: time.Duration(maxVal(cfg.Server.IdleTimeout, 120)) * time.Second, } // 启动 HTTP 服务(非阻塞) go func() { log.Printf("服务启动,监听端口 :%d", port) log.Println("网站入口:") log.Println(" 企业官网 A (科技): http://localhost:8080/site1") log.Println(" 企业官网 B (制造): http://localhost:8080/site2") log.Println(" 企业官网 C (咨询): http://localhost:8080/site3") log.Println(" 跨境电商 A (红色): http://localhost:8080/shop") log.Println(" 跨境电商 B (暗黑): http://localhost:8080/shop2") log.Println(" 跨境电商 C (北欧): http://localhost:8080/shop3") log.Println(" 外卖平台 A (橙黄): http://localhost:8080/food1") log.Println(" 外卖平台 B (深色): http://localhost:8080/food2") log.Println(" 外卖平台 C (清新): http://localhost:8080/food3") log.Println(" 游戏平台 A (赛博): http://localhost:8080/game1") log.Println(" 游戏平台 B (像素): http://localhost:8080/game2") log.Println(" 游戏平台 C (云游): http://localhost:8080/game3") log.Println(" 应用商店 A (极简): http://localhost:8080/app1") log.Println(" 应用商店 B (暗黑): http://localhost:8080/app2") log.Println(" 应用商店 C (清新): http://localhost:8080/app3") log.Println(" 音乐平台 A (暗黑): http://localhost:8080/music1") log.Println(" 音乐平台 B (黑胶): http://localhost:8080/music2") log.Println(" 音乐平台 C (渐变): http://localhost:8080/music3") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("服务启动失败: %v", err) } }() // 等待中断信号(SIGINT / SIGTERM) quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("收到关机信号,开始优雅关机...") // 给正在处理的请求完成时间(从 config.yaml 读取,默认 15 秒) shutdownTimeout := maxVal(cfg.Server.ShutdownTimeout, 15) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(shutdownTimeout)*time.Second) defer cancel() // 优雅关机顺序:停止接收新请求 → 等待进行中的请求完成 → 停止定时任务 → 关闭数据库 if err := srv.Shutdown(ctx); err != nil { log.Printf("HTTP 服务关机超时: %v", err) } log.Println("HTTP 服务已停止") scheduler.Stop() log.Println("定时任务已停止") if err := db.Close(); err != nil { log.Printf("关闭数据库失败: %v", err) } log.Println("数据库连接已关闭") log.Println("优雅关机完成,再见") } // maxVal 返回 a 和 fallback 中的较大值(用于配置默认值兜底) func maxVal(a, fallback int) int { if a > 0 { return a } return fallback } // setupFrontend 配置嵌入式前端:4 套网站模板 + 静态资源 + SPA 回退 func setupFrontend(r *gin.Engine) { distFS, err := fs.Sub(webDist, "web/dist") if err != nil { log.Printf("嵌入式前端不可用: %v", err) return } // 读取各站点 index.html adminIndex, _ := fs.ReadFile(distFS, "admin/index.html") site1Index, _ := fs.ReadFile(distFS, "site1/index.html") site2Index, _ := fs.ReadFile(distFS, "site2/index.html") site3Index, _ := fs.ReadFile(distFS, "site3/index.html") shopIndex, _ := fs.ReadFile(distFS, "shop/index.html") shop2Index, _ := fs.ReadFile(distFS, "shop2/index.html") shop3Index, _ := fs.ReadFile(distFS, "shop3/index.html") food1Index, _ := fs.ReadFile(distFS, "food1/index.html") food2Index, _ := fs.ReadFile(distFS, "food2/index.html") food3Index, _ := fs.ReadFile(distFS, "food3/index.html") game1Index, _ := fs.ReadFile(distFS, "game1/index.html") game2Index, _ := fs.ReadFile(distFS, "game2/index.html") game3Index, _ := fs.ReadFile(distFS, "game3/index.html") app1Index, _ := fs.ReadFile(distFS, "app1/index.html") app2Index, _ := fs.ReadFile(distFS, "app2/index.html") app3Index, _ := fs.ReadFile(distFS, "app3/index.html") music1Index, _ := fs.ReadFile(distFS, "music1/index.html") music2Index, _ := fs.ReadFile(distFS, "music2/index.html") music3Index, _ := fs.ReadFile(distFS, "music3/index.html") rootIndex, _ := fs.ReadFile(distFS, "index.html") // 各站点共享的静态资源目录 sharedFS, _ := fs.Sub(distFS, "shared") r.StaticFS("/shared", http.FS(sharedFS)) // /assets/* 静态资源(Vue 构建产物,如果存在) if assetsFS, err := fs.Sub(distFS, "assets"); err == nil { r.StaticFS("/assets", http.FS(assetsFS)) } // favicon if favicon, err := fs.ReadFile(distFS, "favicon.ico"); err == nil { r.GET("/favicon.ico", func(c *gin.Context) { c.Data(http.StatusOK, "image/x-icon", favicon) }) } // 站点入口路由 r.GET("/", func(c *gin.Context) { if len(rootIndex) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", rootIndex) } else { // 默认跳转到 site1 c.Redirect(http.StatusFound, "/site1") } }) // 管理后台入口 if len(adminIndex) > 0 { r.GET("/admin", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", adminIndex) }) } // 每个站点的 index.html if len(site1Index) > 0 { r.GET("/site1", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", site1Index) }) } if len(site2Index) > 0 { r.GET("/site2", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", site2Index) }) } if len(site3Index) > 0 { r.GET("/site3", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", site3Index) }) } if len(shopIndex) > 0 { r.GET("/shop", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", shopIndex) }) } if len(shop2Index) > 0 { r.GET("/shop2", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", shop2Index) }) } if len(shop3Index) > 0 { r.GET("/shop3", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", shop3Index) }) } if len(food1Index) > 0 { r.GET("/food1", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", food1Index) }) } if len(food2Index) > 0 { r.GET("/food2", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", food2Index) }) } if len(food3Index) > 0 { r.GET("/food3", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", food3Index) }) } if len(game1Index) > 0 { r.GET("/game1", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", game1Index) }) } if len(game2Index) > 0 { r.GET("/game2", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", game2Index) }) } if len(game3Index) > 0 { r.GET("/game3", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", game3Index) }) } if len(app1Index) > 0 { r.GET("/app1", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", app1Index) }) } if len(app2Index) > 0 { r.GET("/app2", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", app2Index) }) } if len(app3Index) > 0 { r.GET("/app3", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", app3Index) }) } if len(music1Index) > 0 { r.GET("/music1", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", music1Index) }) } if len(music2Index) > 0 { r.GET("/music2", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", music2Index) }) } if len(music3Index) > 0 { r.GET("/music3", func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", music3Index) }) } // SPA 回退:非 API 路由返回对应站点页面 r.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path // API 路由返回 404 JSON if strings.HasPrefix(path, "/api/") { c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "接口不存在", "data": nil}) return } // 根据路径前缀返回对应站点页面 switch { case strings.HasPrefix(path, "/admin"): if len(adminIndex) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", adminIndex) return } case strings.HasPrefix(path, "/site1"): if len(site1Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", site1Index) return } case strings.HasPrefix(path, "/site2"): if len(site2Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", site2Index) return } case strings.HasPrefix(path, "/site3"): if len(site3Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", site3Index) return } case strings.HasPrefix(path, "/shop2"): if len(shop2Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", shop2Index) return } case strings.HasPrefix(path, "/shop3"): if len(shop3Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", shop3Index) return } case strings.HasPrefix(path, "/shop"): if len(shopIndex) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", shopIndex) return } case strings.HasPrefix(path, "/food1"): if len(food1Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", food1Index) return } case strings.HasPrefix(path, "/food2"): if len(food2Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", food2Index) return } case strings.HasPrefix(path, "/food3"): if len(food3Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", food3Index) return } case strings.HasPrefix(path, "/game1"): if len(game1Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", game1Index) return } case strings.HasPrefix(path, "/game2"): if len(game2Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", game2Index) return } case strings.HasPrefix(path, "/game3"): if len(game3Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", game3Index) return } case strings.HasPrefix(path, "/app1"): if len(app1Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", app1Index) return } case strings.HasPrefix(path, "/app2"): if len(app2Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", app2Index) return } case strings.HasPrefix(path, "/app3"): if len(app3Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", app3Index) return } case strings.HasPrefix(path, "/music1"): if len(music1Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", music1Index) return } case strings.HasPrefix(path, "/music2"): if len(music2Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", music2Index) return } case strings.HasPrefix(path, "/music3"): if len(music3Index) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", music3Index) return } } // 默认回退 if len(rootIndex) > 0 { c.Data(http.StatusOK, "text/html; charset=utf-8", rootIndex) } else { c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "页面不存在", "data": nil}) } }) }