package main import ( "context" "log" "net/http" "os" "path/filepath" "strings" "time" "yh_web/server/config" "yh_web/server/handlers" "yh_web/server/middleware" "yh_web/server/models" "yh_web/server/pkg/logger" "yh_web/server/pkg/schema" "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) // loadEnv 启动时自动加载 .env(在 server 目录或项目根/server 下),不覆盖已存在的环境变量 func loadEnv() { wd, _ := os.Getwd() serverDir := wd if !strings.HasSuffix(filepath.Clean(wd), "server") { serverDir = filepath.Join(wd, "server") } envPath := filepath.Clean(filepath.Join(serverDir, ".env")) if _, err := os.Stat(envPath); err == nil { if err := godotenv.Load(envPath); err == nil { log.Printf("已加载配置: %s", envPath) } } } func main() { loadEnv() // 初始化根目录 logs/server(支持从项目根或 server 目录启动) wd, _ := os.Getwd() baseDir := filepath.Join(wd, "logs", "server") if strings.HasSuffix(filepath.Clean(wd), "server") { baseDir = filepath.Join(wd, "..", "logs", "server") } logger.Init(filepath.Clean(baseDir)) // 连接 MongoDB;URI 从环境变量 MONGODB_URI 读取,默认 mongodb://localhost:27017;SKIP_MONGODB=1 时可跳过 if os.Getenv("SKIP_MONGODB") != "1" { mongoURI := os.Getenv("MONGODB_URI") if mongoURI == "" { mongoURI = "mongodb://localhost:27017" } if dbName := os.Getenv("MONGODB_DB"); dbName != "" { config.DBName = dbName } if err := config.ConnectMongoDB(mongoURI); err != nil { logger.Err("main", "MongoDB 连接失败: %v(若仅需健康检查可设置环境变量 SKIP_MONGODB=1 后启动)", err) log.Fatalf("MongoDB 连接失败: %v(若仅需健康检查可设置环境变量 SKIP_MONGODB=1 后启动)", err) } defer config.CloseMongoDB() // 启动时获取线上表结构并生成 sql;缺失的集合在线上创建并生成 created_*.sql projectRoot := wd if strings.HasSuffix(filepath.Clean(wd), "server") { projectRoot = filepath.Join(wd, "..") } projectRoot = filepath.Clean(projectRoot) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) if err := schema.Sync(ctx, projectRoot); err != nil { logger.Err("main", "启动时同步表结构失败: %v", err) log.Printf("警告: 启动时同步表结构失败: %v", err) } cancel() } else { logger.Log("main", "已跳过 MongoDB 连接(SKIP_MONGODB=1),仅 /api/health 等不依赖数据库的接口可用") log.Println("已跳过 MongoDB 连接(SKIP_MONGODB=1),仅 /api/health 等不依赖数据库的接口可用") } r := gin.Default() r.MaxMultipartMemory = 200 << 20 // 200MB,与 Nginx client_max_body_size 一致,避免上传 413 r.Use(middleware.ErrorLogger()) // CORS(ALLOWED_ORIGINS 为空则允许所有来源;否则仅允许配置的域名) allowedOriginsEnv := os.Getenv("ALLOWED_ORIGINS") r.Use(func(c *gin.Context) { origin := c.GetHeader("Origin") if allowedOriginsEnv != "" { for _, o := range strings.Split(allowedOriginsEnv, ",") { if strings.TrimSpace(o) == origin { c.Header("Access-Control-Allow-Origin", origin) break } } } else { c.Header("Access-Control-Allow-Origin", "*") } c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) return } c.Next() }) // 未连接 MongoDB 时,/api/admin 下所有接口返回 503(健康检查不受影响) r.Use(func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api/admin") && config.MongoClient == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "服务暂不可用,数据库未连接"}) c.Abort() return } c.Next() }) // 健康检查 r.GET("/api/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) // 登录、注册(无需鉴权) r.POST("/api/admin/login", handlers.Login) r.POST("/api/admin/send-code", handlers.SendCode) r.POST("/api/admin/register", handlers.Register) r.POST("/api/admin/reset-password", handlers.ResetPassword) // 后台 API 路由组(需鉴权) admin := r.Group("/api/admin") admin.Use(handlers.AuthRequired()) { admin.GET("/info", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "admin api"}) }) admin.GET("/my-permissions", handlers.GetMyPermissions) admin.GET("/db-structure", func(c *gin.Context) { structure, err := config.GetDBStructure() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, structure) }) admin.GET("/stats", handlers.GetStats) // 用户管理 admin.GET("/users", handlers.RequirePermission(models.PermUserManage), handlers.GetUsers) admin.GET("/users/:id", handlers.RequirePermission(models.PermUserManage), handlers.GetUserByID) admin.POST("/users", handlers.RequirePermission(models.PermUserManage), handlers.CreateUser) admin.PUT("/users/:id", handlers.RequirePermission(models.PermUserManage), handlers.UpdateUser) admin.DELETE("/users/:id", handlers.RequirePermission(models.PermUserManage), handlers.DeleteUser) // 工作空间 admin.GET("/workspaces", handlers.RequirePermission(models.PermWorkspaceManage), handlers.GetWorkspaces) // 对话 admin.GET("/conversations", handlers.RequirePermission(models.PermConversationManage), handlers.GetConversations) // 站点管理(带子路径的路由放前面;与 :site_id 统一,避免 Gin 路由冲突) admin.GET("/sites/:site_id/homepage/download", handlers.RequirePermission(models.PermHomepageEdit), handlers.DownloadHomepage) admin.GET("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.GetHomepage) admin.PUT("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.UpdateHomepage) admin.GET("/sites/:site_id/assets/downloadable", handlers.RequirePermission(models.PermHomepageEdit), handlers.ListDownloadableAssets) admin.GET("/sites/:site_id/assets", handlers.RequirePermission(models.PermSiteManage), handlers.ListSiteAssets) admin.POST("/sites/:site_id/assets", handlers.RequirePermission(models.PermModuleUpload), handlers.UploadSiteAsset) admin.POST("/sites/:site_id/folders", handlers.RequirePermission(models.PermModuleUpload), handlers.CreateSiteFolder) admin.DELETE("/sites/:site_id/assets/:asset_id", handlers.RequirePermission(models.PermSiteManage), handlers.DeleteSiteAsset) admin.GET("/sites", handlers.RequirePermission(models.PermSiteManage), handlers.GetSites) admin.GET("/sites/:site_id", handlers.RequirePermission(models.PermSiteManage), handlers.GetSiteByID) admin.POST("/sites", handlers.RequirePermission(models.PermSiteManage), handlers.CreateSite) admin.PUT("/sites/:site_id", handlers.RequirePermission(models.PermSiteManage), handlers.UpdateSite) admin.DELETE("/sites/:site_id", handlers.RequirePermission(models.PermSiteManage), handlers.DeleteSite) admin.GET("/system/official-site", handlers.RequirePermission(models.PermSiteManage), handlers.GetOfficialSite) admin.PUT("/system/official-site", handlers.RequirePermission(models.PermSiteManage), handlers.SetOfficialSite) // 角色权限管理 admin.GET("/role-permissions", handlers.RequirePermission(models.PermRolePermission), handlers.GetRolePermissionsList) admin.POST("/role-permissions", handlers.RequirePermission(models.PermRolePermission), handlers.CreateRole) admin.PUT("/role-permissions/:role_id", handlers.RequirePermission(models.PermRolePermission), handlers.UpdateRolePermissions) admin.DELETE("/role-permissions/:role_id", handlers.RequirePermission(models.PermRolePermission), handlers.DeleteRole) // 网页管理(按站点) admin.GET("/pages", handlers.RequirePermission(models.PermPageManage), handlers.GetPages) admin.GET("/pages/:id", handlers.RequirePermission(models.PermPageManage), handlers.GetPageByID) admin.POST("/pages", handlers.RequirePermission(models.PermPageManage), handlers.CreatePage) admin.PUT("/pages/:id", handlers.RequirePermission(models.PermPageManage), handlers.UpdatePage) admin.DELETE("/pages/:id", handlers.RequirePermission(models.PermPageManage), handlers.DeletePage) // 短信平台配置 smsConfig := admin.Group("/sms-config") smsConfig.Use(handlers.RequirePermission(models.PermSMSConfig)) { smsConfig.GET("", handlers.GetSMSConfig) smsConfig.PUT("", handlers.UpdateSMSConfig) } // 支付配置(微信、支付宝) paymentConfig := admin.Group("/payment-config") paymentConfig.Use(handlers.RequirePermission(models.PermPaymentConfig)) { paymentConfig.GET("", handlers.GetPaymentConfig) paymentConfig.PUT("", handlers.UpdatePaymentConfig) } } // 官网站点首页(前台,无需鉴权) r.GET("/api/web/homepage", handlers.GetWebHomepage) r.GET("/api/web/routes", handlers.GetWebRoutes) r.GET("/api/web/page", handlers.GetWebPageByPath) // 前台 API 路由组 web := r.Group("/api/web") { web.GET("/info", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "web api"}) }) // 可下载资源公开下载(首页等链接指向此路径) web.GET("/sites/:site_id/assets/:asset_id/download", handlers.DownloadSiteAsset) } port := os.Getenv("PORT") if port == "" { port = "8080" } r.Run(":" + port) }