Add main.go

This commit is contained in:
ata
2026-07-10 10:05:48 +00:00
committed by Gitee
parent 76cca713b0
commit 5864b56c93

450
main.go Normal file
View File

@@ -0,0 +1,450 @@
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})
}
})
}