452 lines
14 KiB
Go
452 lines
14 KiB
Go
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)
|
||
uploadService.EnsureUploadDir() // 创建各模块子目录
|
||
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})
|
||
}
|
||
})
|
||
}
|