package main import ( "embed" "io/fs" "log" "mime" "net/http" "os" "path/filepath" "strings" "time" _ "time/tzdata" "home-vue-go/internal/api" "home-vue-go/internal/config" "home-vue-go/internal/database" "github.com/gin-gonic/gin" ) //go:embed dist/* var distFS embed.FS func main() { loc, err := time.LoadLocation("Asia/Shanghai") if err != nil { log.Fatal("无法加载时区:", err) } time.Local = loc exePath, err := os.Executable() if err != nil { log.Fatal("无法获取可执行文件路径:", err) } dataDir := filepath.Join(filepath.Dir(exePath), "data") if err := os.MkdirAll(dataDir, 0755); err != nil { log.Fatal("无法创建data目录:", err) } cfg := config.New(dataDir) db, err := database.Init(cfg.DatabasePath, cfg) if err != nil { log.Fatal("数据库初始化失败:", err) } defer db.Close() if os.Getenv("GIN_MODE") == "" { gin.SetMode(gin.ReleaseMode) } port := os.Getenv("PORT") if port == "" { port = "1552" } router := gin.New() router.Use(gin.Recovery(), corsMiddleware(port)) api.SetupRoutes(router, db, cfg) distRoot, err := fs.Sub(distFS, "dist") if err != nil { log.Fatal("无法加载嵌入的前端文件:", err) } router.NoRoute(serveFrontend(distRoot)) log.Println("使用嵌入的前端文件(单一可执行文件模式)") firstRunFile := filepath.Join(dataDir, ".first_run") if _, err := os.Stat(firstRunFile); os.IsNotExist(err) { if err := os.WriteFile(firstRunFile, []byte(""), 0644); err != nil { log.Printf("记录首次启动状态失败: %v", err) } log.Printf("默认管理员账号: admin, 密码: admin123") log.Printf("提示: 首次启动后,请及时修改默认密码以确保安全") } log.Printf("========================================") log.Printf("服务器启动成功!") log.Printf("访问端点: http://localhost:%s", port) log.Printf("========================================") server := &http.Server{ Addr: ":" + port, Handler: router, } if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("服务器启动失败: %v", err) } } func serveFrontend(root fs.FS) gin.HandlerFunc { return func(c *gin.Context) { requestPath := strings.TrimPrefix(c.Request.URL.Path, "/") if requestPath == "api" || strings.HasPrefix(requestPath, "api/") || requestPath == "uploads" || strings.HasPrefix(requestPath, "uploads/") { c.Status(http.StatusNotFound) return } if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead { c.Status(http.StatusNotFound) return } filePath := requestPath if filePath == "" { filePath = "index.html" } if serveEmbeddedFile(c, root, filePath) { return } if !serveEmbeddedFile(c, root, "index.html") { c.Status(http.StatusNotFound) } } } func serveEmbeddedFile(c *gin.Context, root fs.FS, filePath string) bool { file, err := root.Open(filePath) if err != nil { return false } defer file.Close() stat, err := file.Stat() if err != nil || stat.IsDir() { return false } content, err := fs.ReadFile(root, filePath) if err != nil { return false } contentType := mime.TypeByExtension(filepath.Ext(filePath)) if contentType == "" { contentType = http.DetectContentType(content) } c.Data(http.StatusOK, contentType, content) return true } func corsMiddleware(port string) gin.HandlerFunc { allowedOrigins := make(map[string]struct{}) for _, origin := range strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ",") { if value := strings.TrimSpace(origin); value != "" { allowedOrigins[value] = struct{}{} } } if len(allowedOrigins) == 0 { allowedOrigins["http://localhost:"+port] = struct{}{} allowedOrigins["http://127.0.0.1:"+port] = struct{}{} } return func(c *gin.Context) { origin := c.GetHeader("Origin") if _, ok := allowedOrigins[origin]; ok { c.Writer.Header().Set("Access-Control-Allow-Origin", origin) c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Add("Vary", "Origin") } c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH") if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) return } c.Next() } }