Files
web/server/main.go

237 lines
9.8 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"
"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"
"yh_web/server/pkg/weblive"
"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))
// 连接 MongoDBURI 从环境变量 MONGODB_URI 读取,默认 mongodb://localhost:27017SKIP_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 = 32 << 20 // 大单片先落临时文件;整体体积受 Nginx client_max_body_size 限制
r.Use(middleware.ErrorLogger())
// CORSALLOWED_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"})
})
// 推广/产品视频:站点 uploads 下 promotion 目录(无需鉴权,与后台上传到 promotion/… 对应)
web.GET("/sites/:site_id/promotion-media/*filepath", handlers.ServePromotionMedia)
// 可下载资源公开下载(首页等链接指向此路径)
web.GET("/sites/:site_id/assets/:asset_id/download", handlers.DownloadSiteAsset)
// 站内 WebRTC 直播:信令 + 状态(单房间 MVP
weblive.RegisterRoutes(web)
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// 启动后扫描 uploads 下各站点 promotion 中遗留的 .mov 并补转码(与上传后异步转码逻辑一致)
go handlers.SweepPromotionTranscodeOnStartup()
r.Run(":" + port)
}