新增 needFill 辅助函数统一判断某个配置项是否「等于没配」:
配置项不存在、值为空串、或仍是未被替换的 ${PLACEHOLDER} 三种情况。
背景:gf v2.10.2 起不再自动替换 ${ENV} 占位符,且容器内跑的
config.yaml 可能是旧版(没有 wallpaper 配置段)。原先只判断
是否含 "${" 会在「配置段整体缺失」时漏判,导致 wallpaper.root
与 wallpaper.baseUrl 拿不到兜底值。
adapter.Get 返回 any 而非 *gvar.Var,故取值统一走 gconv.String。
220 lines
11 KiB
Go
220 lines
11 KiB
Go
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())
|
||
}
|
||
}
|
||
|
||
// needFill 判断某个配置项是否「等于没配」:不存在、为空串、或仍是未被替换的
|
||
// ${PLACEHOLDER}。gf v2.10.2 起不再自动替换占位符,容器里跑的 config.yaml
|
||
// 也可能是旧版(没有对应的配置段),这两种情况都必须回填兜底值。
|
||
//
|
||
// 注意 adapter.Get 返回的是 any 而不是 *gvar.Var,取值统一走 gconv。
|
||
func needFill(ctx context.Context, adapter *gcfg.AdapterFile, key string) bool {
|
||
v, err := adapter.Get(ctx, key)
|
||
if err != nil || v == nil {
|
||
return true
|
||
}
|
||
s := gconv.String(v)
|
||
return s == "" || gstr.Contains(s, "${")
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
// 壁纸库路径同理,但要处理**三种**缺失形态:配置项不存在(容器里跑的
|
||
// config.yaml 可能是旧版、压根没有 wallpaper 段)、值为空、以及占位符
|
||
// 没被替换。任何一种漏掉都会掉进 NewStorage 的相对路径兜底
|
||
// (容器内 ./data/wallpaper),表现为「上传成功但 nginx 找不到文件」,
|
||
// 而且重启容器就全丢 —— 这种故障极难排查,所以这里必须兜死。
|
||
// 生产兜底到 /data/www/wallpaper(由 nginx 直出),开发兜底到仓库内 data/。
|
||
if needFill(ctx, adapter, "wallpaper.root") {
|
||
if isProd {
|
||
_ = adapter.Set("wallpaper.root", "/data/www/wallpaper")
|
||
} else {
|
||
_ = adapter.Set("wallpaper.root", "./data/wallpaper")
|
||
}
|
||
}
|
||
if needFill(ctx, adapter, "wallpaper.baseUrl") {
|
||
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
|
||
},
|
||
}
|
||
)
|