Files
web_template/main.go

471 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/geoip"
"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)
customerHandler := handler.NewCustomerHandler(db.DB, cfg)
accessLogHandler := handler.NewAccessLogHandler(db.DB)
configHandler := handler.NewConfigHandler(db.DB)
// IP 归属地查询服务
ipLocationService := geoip.NewService()
// 8. 设置路由
r := router.Setup(cfg, db, authHandler, userHandler, adminHandler, uploadHandler, enterpriseHandler, shopHandler, foodHandler, gameHandler, appHandler, musicHandler, dashboardHandler, customerHandler, accessLogHandler, configHandler, ipLocationService)
// 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(" 跨境电商 D (古树): http://localhost:8080/shop4")
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")
shop4Index, _ := fs.ReadFile(distFS, "shop4/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(shop4Index) > 0 {
r.GET("/shop4", func(c *gin.Context) {
c.Data(http.StatusOK, "text/html; charset=utf-8", shop4Index)
})
}
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, "/shop4"):
if len(shop4Index) > 0 {
c.Data(http.StatusOK, "text/html; charset=utf-8", shop4Index)
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})
}
})
}