package main import ( "context" "os" "strings" _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" "36wisdom/biz/consts" "36wisdom/biz/controller" "36wisdom/biz/dao" "36wisdom/common" "36wisdom/common/auth" "36wisdom/seed" ) func main() { var ctx context.Context = gctx.New() for _, dir := range []string{"data", "workspace", "workspace/uploads/image", "workspace/uploads/audio"} { if err := os.MkdirAll(dir, 0o755); err != nil { g.Log().Fatal(ctx, err) } } initTables(ctx) seed.EnsureSeeded(ctx) s := g.Server() // 路由注册与中间件装配(静态,不属业务表分层) s.Use(common.MiddlewareNoiseSilence) s.Use(ghttp.MiddlewareHandlerResponse) // 静态生成资产(PNG/音频)路径不可变:强缓存,客户端每文件只下载一次。 // 静态文件服务不走中间件链,须用 HookBeforeServe(对静态/动态请求均执行) s.BindHookHandler("/*", ghttp.HookBeforeServe, func(r *ghttp.Request) { if strings.HasPrefix(r.URL.Path, "/static/") { r.Response.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } }) s.BindHandler("GET:/ping", func(r *ghttp.Request) { r.Response.Write("pong") }) secret := g.Cfg().MustGet(ctx, "auth.secret").String() // 前台:公开接口(注册/登录),鉴权接口按模块挂载 s.Group("/api", func(g *ghttp.RouterGroup) { g.Bind(controller.Parent) g.Group("/", func(g *ghttp.RouterGroup) { g.Middleware(auth.Middleware(secret, "")) g.Bind(controller.Child, controller.Strategy, controller.Level) }) }) // 后台:登录公开,其余 admin 角色鉴权 s.Group("/api/admin", func(g *ghttp.RouterGroup) { g.Bind(controller.AdminUser) g.Group("/", func(g *ghttp.RouterGroup) { g.Middleware(auth.Middleware(secret, consts.RoleAdmin)) g.Bind(controller.AdminUpload, controller.AdminElement, controller.AdminStrategy, controller.AdminLevel, controller.AdminSceneNode, controller.AdminNodeOption, controller.AdminPrize, controller.AdminBadge, controller.AdminLifeTask, controller.AdminParent, controller.AdminChild, controller.AdminRedemption, controller.AdminStats) }) }) // 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口) // 注:根路径须用 SetServerRoot——AddStaticPath("/", ...) 因前缀防误匹配守卫只服务根路径, // /assets 等子路径全部 404;素材目录 ui-src/static 以 /static 前缀单独挂载 if _, err := os.Stat("ui-src/dist"); err == nil { s.SetServerRoot("ui-src/dist") } if _, err := os.Stat("ui-src/static"); err == nil { s.AddStaticPath("/static", "ui-src/static") } // 后台上传素材:/uploads 前缀(可变内容,不强缓存) s.AddStaticPath("/uploads", "workspace/uploads") // 管理后台产物 admin-src/dist(hash 路由,由 /admin 前缀托管) if _, err := os.Stat("admin-src/dist"); err == nil { s.AddStaticPath("/admin", "admin-src/dist") } s.Run() } // initTables 建表与死表清理(各 dao 的 Init 幂等;life_task_log 无读写引用整套移除, // 库内残留表启动时 DROP 与代码保持一致)。 func initTables(ctx context.Context) { inits := []func(ctx context.Context) error{ dao.Parent.Init, dao.Child.Init, dao.Strategy.Init, dao.Level.Init, dao.SceneNode.Init, dao.NodeOption.Init, dao.Element.Init, dao.UserProgress.Init, dao.ChapterReview.Init, dao.UserRouteLog.Init, dao.UserCollection.Init, dao.Badge.Init, dao.UserBadge.Init, dao.Prize.Init, dao.Redemption.Init, dao.AdminUser.Init, dao.LifeTask.Init, } for _, init := range inits { if err := init(ctx); err != nil { g.Log().Fatal(ctx, err) } } if _, err := g.DB().Exec(ctx, "DROP TABLE IF EXISTS life_task_log"); err != nil { g.Log().Fatal(ctx, err) } }