package cmd import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/genv" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" adminctl "service.xpcool.com/internal/controller/admin" housectl "service.xpcool.com/internal/controller/house" jobctl "service.xpcool.com/internal/controller/job" noticectl "service.xpcool.com/internal/controller/notice" openctl "service.xpcool.com/internal/controller/open" recruitmentctl "service.xpcool.com/internal/controller/recruitment" serversecurityctl "service.xpcool.com/internal/controller/serversecurity" userctl "service.xpcool.com/internal/controller/user" wallpaperctl "service.xpcool.com/internal/controller/wallpaper" "service.xpcool.com/internal/library/crypto" "service.xpcool.com/internal/library/jwt" "service.xpcool.com/internal/middleware" admin "service.xpcool.com/internal/service/admin/admin/admin" adminauth "service.xpcool.com/internal/service/admin/admin/login" log "service.xpcool.com/internal/service/admin/base/log" adminaudit "service.xpcool.com/internal/service/admin/system/log" adminloginlog "service.xpcool.com/internal/service/admin/system/login_log" adminmenu "service.xpcool.com/internal/service/admin/system/menu" menu_manage "service.xpcool.com/internal/service/admin/system/menu_manage" role "service.xpcool.com/internal/service/admin/system/role" housecommunity "service.xpcool.com/internal/service/house/community" housedashboard "service.xpcool.com/internal/service/house/dashboard" houselisting "service.xpcool.com/internal/service/house/listing" housepresale "service.xpcool.com/internal/service/house/presale" housetransaction "service.xpcool.com/internal/service/house/transaction" jobsvc "service.xpcool.com/internal/service/job" noticesvc "service.xpcool.com/internal/service/notice" recruitmentsvc "service.xpcool.com/internal/service/recruitment" serversecuritysvc "service.xpcool.com/internal/service/serversecurity" userauth "service.xpcool.com/internal/service/user/auth" wallpapersvc "service.xpcool.com/internal/service/wallpaper" ) // 开发环境默认值:任何启动方式(GoLand / 命令行 / go run)漏注入环境变量时兜底, // 避免 ${DB_DSN} 占位符原样传入 gdb 导致全站接口 500。 // 生产(GF_GCFG_ENV=prod)不启用兜底,配置缺失应显性报错。 // // 注意:库名以本机实际建库为准(本项目为 service / recruitment)。 // 若你的本地库名或账号口令不同,请通过 DB_DSN / RECRUITMENT_DB_DSN 覆盖。 const ( devDefaultDBDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/service?loc=Local" devDefaultRecruitmentDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/recruitment?loc=Local" devDefaultJWTSecret = "dev-only-secret-not-for-production" ) // setIfEmpty 仅当 env 存在时写入配置(优先级:env > 配置文件默认值)。 func setIfEmpty(adapter *gcfg.AdapterFile, key, envName string) { if v := genv.Get(envName); !v.IsEmpty() { _ = adapter.Set(key, v.String()) } } // injectEnv 手动把关键环境变量写入配置系统。 // 注意:gf v2.10.2 起配置不再自动替换 ${ENV} 占位符,需在此显式注入, // 否则 config.dev.yaml 中的 ${DB_DSN}/${JWT_SECRET} 会原样传给数据库与 JWT。 // 开发环境下若 env 缺失,回填本地默认值,保证「零配置可启动」。 func injectEnv(ctx context.Context) { adapter, ok := g.Cfg().GetAdapter().(*gcfg.AdapterFile) if !ok { return } isProd := genv.Get("GF_GCFG_ENV").String() == "prod" if !isProd { // 开发兜底:仅当配置项仍是未解析的 ${...} 占位符时才写默认值, // 不覆盖 .env.dev / GoLand 运行配置注入的真实值。 if v, _ := adapter.Get(ctx, "database.default.link"); v != nil && gstr.Contains(gconv.String(v), "${") { _ = adapter.Set("database.default.link", devDefaultDBDSN) } if v, _ := adapter.Get(ctx, "database.recruitment.link"); v != nil && gstr.Contains(gconv.String(v), "${") { _ = adapter.Set("database.recruitment.link", devDefaultRecruitmentDSN) } if v, _ := adapter.Get(ctx, "jwt.secret"); v != nil && gstr.Contains(gconv.String(v), "${") { _ = adapter.Set("jwt.secret", devDefaultJWTSecret) } } // 壁纸库路径同理:占位符没被注入时要回填,否则会真的创建一个名为 // "${WALLPAPER_ROOT}" 的目录,图片全落到那里且难以察觉。 // 生产兜底到 /data/www/wallpaper(由 nginx 直出),开发兜底到仓库内 data/。 if v, _ := adapter.Get(ctx, "wallpaper.root"); v != nil && gstr.Contains(gconv.String(v), "${") { if isProd { _ = adapter.Set("wallpaper.root", "/data/www/wallpaper") } else { _ = adapter.Set("wallpaper.root", "./data/wallpaper") } } if v, _ := adapter.Get(ctx, "wallpaper.baseUrl"); v != nil && gstr.Contains(gconv.String(v), "${") { if isProd { _ = adapter.Set("wallpaper.baseUrl", "https://xpcool.com/wallpaper") } else { _ = adapter.Set("wallpaper.baseUrl", "/wallpaper") } } setIfEmpty(adapter, "database.default.link", "DB_DSN") setIfEmpty(adapter, "jwt.secret", "JWT_SECRET") // 招聘模块独立数据库与 Bark 推送配置(自建 Bark 服务)。 setIfEmpty(adapter, "database.recruitment.link", "RECRUITMENT_DB_DSN") setIfEmpty(adapter, "bark.baseUrl", "BARK_BASE_URL") setIfEmpty(adapter, "bark.deviceKey", "BARK_DEVICE_KEY") setIfEmpty(adapter, "bark.pushTime", "BARK_PUSH_TIME") // 服务器安全日志上报令牌(宿主机采集脚本携带,接口侧校验)。 setIfEmpty(adapter, "internalToken", "INTERNAL_TOKEN") // 登录加密开关(直接跑二进制始终加载 config.yaml,需 env 显式注入): // ENCRYPT_FULL_BODY=true 启用全量请求/响应加密(生产);ENCRYPT_ALLOW_PLAIN=true 仅开发联调。 setIfEmpty(adapter, "encrypt.fullBody", "ENCRYPT_FULL_BODY") setIfEmpty(adapter, "encrypt.allowPlain", "ENCRYPT_ALLOW_PLAIN") // 壁纸图库:上传文件的落盘根目录与对外访问前缀。 // 生产为 /data/www/wallpaper + https://xpcool.com/wallpaper(由 nginx 直出)。 setIfEmpty(adapter, "wallpaper.root", "WALLPAPER_ROOT") setIfEmpty(adapter, "wallpaper.baseUrl", "WALLPAPER_BASE_URL") } var ( Main = gcmd.Command{ Name: "main", Usage: "main", Brief: "start http server", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { injectEnv(ctx) s := g.Server() tokens := jwt.New(ctx) // 登录密码混合加密服务(RSA + AES-GCM):私钥加载/生成失败属致命错误,直接终止启动。 cryptoSvc, err := crypto.New(ctx) if err != nil { panic(err) } userauth.RegisterUserAuth(userauth.NewUserAuth(tokens, nil, nil)) adminauth.RegisterAdminAuth(adminauth.NewAdminAuth(tokens, cryptoSvc)) adminmenu.RegisterAdminMenu(adminmenu.NewAdminMenu()) admin.RegisterAdminManage(admin.NewAdminManage()) role.RegisterRoleManage(role.NewRoleManage()) menu_manage.RegisterMenuManage(menu_manage.NewMenuManage()) log.RegisterLogManage(log.NewLogManage()) adminaudit.RegisterAdminAudit(adminaudit.NewAdminAudit()) adminloginlog.RegisterAdminLoginLog(adminloginlog.NewAdminLoginLog()) housecommunity.RegisterCommunity(housecommunity.NewCommunity()) houselisting.RegisterListing(houselisting.NewListing()) housedashboard.RegisterDashboard(housedashboard.NewDashboard()) housetransaction.RegisterTransaction(housetransaction.NewTransaction()) housepresale.RegisterPresale(housepresale.NewPresale()) recruitmentsvc.RegisterRecruitment(recruitmentsvc.New()) noticesvc.RegisterNotice(noticesvc.New()) jobsvc.RegisterJob(jobsvc.New()) serversecuritysvc.RegisterServerSecurity(serversecuritysvc.New()) wallpapersvc.RegisterWallpaper(wallpapersvc.New(ctx)) s.Group("/api/service/open", func(group *ghttp.RouterGroup) { group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) group.Bind(openctl.New()) // 开放工具接口(前端调用,免鉴权)。 group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。 // 壁纸前台读取:xpcool.com 是纯静态站,浏览器直连本服务取数据,故必须免鉴权。 group.Bind(wallpaperctl.NewOpen()) }) s.Group("/api/service/user", func(group *ghttp.RouterGroup) { group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) group.Bind(userctl.New()) // 登录与刷新令牌接口公开。 group.Group("/", func(protected *ghttp.RouterGroup) { protected.Middleware(middleware.UserAuth(tokens)) }) }) s.Group("/api/service/admin", func(group *ghttp.RouterGroup) { // 全量请求/响应加密(生产 encrypt.fullBody=true):CORS → APICrypto → Recover → HandlerResponse。 // Recover 置于 APICrypto 内层,使 500 异常响应同样被加密;public-key 端点明文豁免。 group.Middleware(middleware.CORS, middleware.APICrypto(cryptoSvc), middleware.Recover, ghttp.MiddlewareHandlerResponse) group.Bind(adminctl.NewAuth()) // 公开端点:仅登录。 group.Group("/", func(profile *ghttp.RouterGroup) { // 仅登录端点:个人资料 / 权限码 / 菜单路由。 profile.Middleware(middleware.AdminAuthOnly(tokens)) profile.Bind(adminctl.NewProfile()) }) group.Group("/", func(protected *ghttp.RouterGroup) { // 受权限保护端点:RBAC 管理、日志等。 // 权限由后端按「方法+路径」自动匹配,无需前端传 X-Permission。 protected.Middleware(middleware.AdminAuth(tokens, adminauth.AdminAuth().PermissionForPath, adminauth.AdminAuth().HasPermission, func(ctx context.Context, id uint64, permission, method, path, ip, param string, duration, status int, userAgent, errorMessage string) { adminaudit.AdminAudit().Record(ctx, adminaudit.AuditEvent{AdminID: id, Permission: permission, Method: method, Path: path, IP: ip, Param: param, DurationMS: duration, StatusCode: status, ErrorMessage: errorMessage, UserAgent: userAgent}) })) protected.Bind(adminctl.New()) protected.Bind(housectl.New()) protected.Bind(recruitmentctl.New()) protected.Bind(noticectl.New()) protected.Bind(jobctl.New()) protected.Bind(serversecurityctl.NewManage()) protected.Bind(wallpaperctl.New()) }) }) // 启动自动任务调度器:招聘模块先把任务注册进来,再由 job 模块按 DB 配置统一调度。 recruitmentsvc.RegisterTasks() jobsvc.Job().StartScheduler(ctx) s.Run() return nil }, } )