签入版本
This commit is contained in:
+4
-4
@@ -59,7 +59,7 @@ RUN mkdir -p /opt/apps
|
|||||||
WORKDIR /opt/apps
|
WORKDIR /opt/apps
|
||||||
|
|
||||||
# 拷贝运行时资产(四件缺一不可,路径相对项目根)
|
# 拷贝运行时资产(四件缺一不可,路径相对项目根)
|
||||||
# manifest/config 运行时配置(敏感项运行时经 GF_GENV_ 环境变量覆盖,不改镜像内文件)
|
# manifest/config 运行时配置(敏感项运行时经 GF_ 环境变量覆盖,不改镜像内文件)
|
||||||
# manifest/data ip2region.xdb 归属地库
|
# manifest/data ip2region.xdb 归属地库
|
||||||
# resource 验证码字体、swagger 页面等静态资源
|
# resource 验证码字体、swagger 页面等静态资源
|
||||||
# templates 代码生成器模板(tool 域运行时读取)
|
# templates 代码生成器模板(tool 域运行时读取)
|
||||||
@@ -84,7 +84,7 @@ RUN set -eux; \
|
|||||||
# 启动命令
|
# 启动命令
|
||||||
# ${GOFILE} 为编译产物文件名(= 产品名)
|
# ${GOFILE} 为编译产物文件名(= 产品名)
|
||||||
# 配置覆盖示例(部署阶段注入,勿写死镜像):
|
# 配置覆盖示例(部署阶段注入,勿写死镜像):
|
||||||
# -e GF_GENV_DATABASE_DEFAULT_LINK="mysql:<用户>:<密码>@tcp(<主机>:<端口>)/<库名>?charset=utf8mb4&parseTime=true&loc=Local"
|
# -e GF_DATABASE_DEFAULT_LINK="mysql:<用户>:<密码>@tcp(<主机>:<端口>)/<库名>?charset=utf8mb4&parseTime=true&loc=Local"
|
||||||
# -e GF_GENV_REDIS_DEFAULT_ADDRESS="<主机>:<端口>"
|
# -e GF_REDIS_DEFAULT_ADDRESS="<主机>:<端口>"
|
||||||
# -e GF_GENV_FILE_UPLOADDIR="/data/upload" # 容器内必须覆盖(配置默认为开发机路径)
|
# -e GF_FILE_UPLOADDIR="/data/upload" # 容器内必须覆盖(配置默认为开发机路径)
|
||||||
CMD ["sh", "-c", "./${GOFILE}"]
|
CMD ["sh", "-c", "./${GOFILE}"]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ GoFrame v2 后端已按 `document/architecture.md`(唯一权威定稿)完整
|
|||||||
|
|
||||||
- **主键统一 int 数据库自增**(2026-10-13 定稿):禁用雪花算法(`utility/id` 已删除);新增后用 `result.LastInsertId()` 回读;hooks 不填 id
|
- **主键统一 int 数据库自增**(2026-10-13 定稿):禁用雪花算法(`utility/id` 已删除);新增后用 `result.LastInsertId()` 回读;hooks 不填 id
|
||||||
- 软删列统一 `del_flag`;表前缀 `goframe_`
|
- 软删列统一 `del_flag`;表前缀 `goframe_`
|
||||||
|
- **配置环境变量覆盖**(2026-10-20 定稿):`utility/config.RegisterEnvOverride()` 在 cmd.Run 最先接线 env-first 适配器(包装默认文件适配器,gcfg.Adapter 官方扩展点),12-Factor 优先级 GF_ 环境变量 > config.yaml;键名=配置路径大写下划线(`database.default.link` → `GF_DATABASE_DEFAULT_LINK`,**不存在 GF_GENV_ 前缀**);仅覆盖配置文件已有键,空值视为未设置;docker 部署经 compose environment 块把 .env 友好名(MYSQL_HOST 等)映射为 GF_ 规范键,勿原名透传
|
||||||
- 新增模块四件套平铺放置(api/v1、controller、logic、service 各一文件,同包 init 自动注册,无需 blank import);service 接口手维护;路由注册由生成器自动写入或手动 `router.go` `Bind(controller.CXxx)`
|
- 新增模块四件套平铺放置(api/v1、controller、logic、service 各一文件,同包 init 自动注册,无需 blank import);service 接口手维护;路由注册由生成器自动写入或手动 `router.go` `Bind(controller.CXxx)`
|
||||||
- **定时任务处理方法在 `internal/task`**:调度器与注册表在 internal/logic 的 job 模块(`logic.RegisterJobHandler`);任务实现平铺 `internal/task/{任务名}.go`,init 中自注册(注册名=url 字段值,http(s) 地址除外),依赖单向 task→logic(任务可复用 base 共享能力,logic 禁止反向 import task);由 `internal/cmd` 空白导入触发加载;调度消费 execute_policy(1并行/2单例/3放弃)与 is_sync(1同步/0异步),策略2/3叠加 `job:lock:{id}` 执行锁(utility/cache SET NX,多实例共享,TTL 300s 兜底);内置 LogClean 任务每日 02:30 分批物理清理三张日志表(保留天数 `job.logRetentionDays` 默认90,任务参数 retentionDays 可覆盖)
|
- **定时任务处理方法在 `internal/task`**:调度器与注册表在 internal/logic 的 job 模块(`logic.RegisterJobHandler`);任务实现平铺 `internal/task/{任务名}.go`,init 中自注册(注册名=url 字段值,http(s) 地址除外),依赖单向 task→logic(任务可复用 base 共享能力,logic 禁止反向 import task);由 `internal/cmd` 空白导入触发加载;调度消费 execute_policy(1并行/2单例/3放弃)与 is_sync(1同步/0异步),策略2/3叠加 `job:lock:{id}` 执行锁(utility/cache SET NX,多实例共享,TTL 300s 兜底);内置 LogClean 任务每日 02:30 分批物理清理三张日志表(保留天数 `job.logRetentionDays` 默认90,任务参数 retentionDays 可覆盖)
|
||||||
- **monitor 域三个无表模块**:在线用户(`/onlineUser/page`+`forceLogout`,登录 MarkOnline/登出 MarkOffline 写 `online:jti` zset+info 键,踢出=拉黑 jti)、服务器监控(`/server/info`:runtime/磁盘分平台文件 server_monitor_windows/other.go/MySQL/Redis 连通)、缓存监控(`/cache/info`:Redis INFO 解析,内存降级时返回 mode=memory);前端 `ui/src/views/monitor/{online,server,cache}`;OpenAPI 经 bind.go 补注册后 `/api.json` 全量可用
|
- **monitor 域三个无表模块**:在线用户(`/onlineUser/page`+`forceLogout`,登录 MarkOnline/登出 MarkOffline 写 `online:jti` zset+info 键,踢出=拉黑 jti)、服务器监控(`/server/info`:runtime/磁盘分平台文件 server_monitor_windows/other.go/MySQL/Redis 连通)、缓存监控(`/cache/info`:Redis INFO 解析,内存降级时返回 mode=memory);前端 `ui/src/views/monitor/{online,server,cache}`;OpenAPI 经 bind.go 补注册后 `/api.json` 全量可用
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ EasyGoAdmin_GoFrame_EleVue/
|
|||||||
│ └── ip2region/ response/ ua # IP 归属地 / 统一响应 / 设备识别
|
│ └── ip2region/ response/ ua # IP 归属地 / 统一响应 / 设备识别
|
||||||
├── templates/ # 代码生成器模板(后端 {{ }} 定界;ui* 前端 [[ ]] 定界)
|
├── templates/ # 代码生成器模板(后端 {{ }} 定界;ui* 前端 [[ ]] 定界)
|
||||||
├── manifest/
|
├── manifest/
|
||||||
│ ├── config/config.yaml # 运行时配置(唯一配置文件,GF_GENV_ 环境变量可覆盖)
|
│ ├── config/config.yaml # 运行时配置(唯一配置文件,GF_ 环境变量可覆盖)
|
||||||
│ └── data/ip2region.xdb # IP 归属地库
|
│ └── data/ip2region.xdb # IP 归属地库
|
||||||
├── resource/
|
├── resource/
|
||||||
│ ├── static/ # 验证码字体等静态资源
|
│ ├── static/ # 验证码字体等静态资源
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
# Docker Compose 环境变量模板
|
# Docker Compose 环境变量模板
|
||||||
# 用法:复制为同目录 .env(同目录被 .gitignore 与 .dockerignore 双重排除)
|
# 用法:复制为同目录 .env(同目录被 .gitignore 与 .dockerignore 双重排除)
|
||||||
# copy .env.example .env
|
# copy .env.example .env
|
||||||
|
# 说明:本文件使用 MYSQL_HOST 等友好名,由 docker-compose.yml 的
|
||||||
|
# environment 块映射为 GF_ 规范键(如 GF_DATABASE_DEFAULT_LINK)
|
||||||
|
# 注入容器——应用只认 GF_ 前缀键,勿将友好名原样透传给应用
|
||||||
|
# 注意:值中含 $ 须写成 $$(compose 变量插值会吞掉单 $,
|
||||||
|
# 如密码 abc#$^ 应写为 abc#$$^)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
# ===== 应用 =====
|
# ===== 应用 =====
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
# - builder 只负责编译,runtime 仅含二进制 + 运行时资产,镜像最小化
|
# - builder 只负责编译,runtime 仅含二进制 + 运行时资产,镜像最小化
|
||||||
# - 运行时资产四件:manifest/config、manifest/data(ip2region.xdb)、
|
# - 运行时资产四件:manifest/config、manifest/data(ip2region.xdb)、
|
||||||
# resource(字体/swagger)、templates(代码生成器模板)
|
# resource(字体/swagger)、templates(代码生成器模板)
|
||||||
# - 敏感配置不改镜像内文件,运行时用 GF_GENV_ 前缀环境变量覆盖
|
# - 敏感配置不改镜像内文件,运行时用 GF_ 前缀环境变量覆盖
|
||||||
# (见 manifest/config/config.yaml 头部说明)
|
# (见 manifest/config/config.yaml 头部说明)
|
||||||
# - 源码 COPY 放在依赖下载之后,保护 go mod 层缓存
|
# - 源码 COPY 放在依赖下载之后,保护 go mod 层缓存
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8000}:8000"
|
- "${APP_PORT:-8000}:8000"
|
||||||
# 敏感/环境相关配置一律 GF_GENV_ 前缀覆盖,不改镜像内 config.yaml
|
# 敏感/环境相关配置一律 GF_ 前缀覆盖,不改镜像内 config.yaml
|
||||||
# (GF_GENV_ 键名 = 配置路径大写下划线,如 file.uploadDir → GF_GENV_FILE_UPLOADDIR)
|
# (GF_ 键名 = 配置路径大写下划线,如 file.uploadDir → GF_FILE_UPLOADDIR)
|
||||||
environment:
|
environment:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
GF_GENV_DATABASE_DEFAULT_LINK: "mysql:${MYSQL_USER:-root}:${MYSQL_PASSWORD:-root}@tcp(${MYSQL_HOST:-mysql}:${MYSQL_HOST_PORT:-3306})/${MYSQL_DATABASE:-easygoadmin.goframe.elevue}?charset=utf8mb4&parseTime=true&loc=Local"
|
GF_DATABASE_DEFAULT_LINK: "mysql:${MYSQL_USER:-root}:${MYSQL_PASSWORD:-root}@tcp(${MYSQL_HOST:-mysql}:${MYSQL_HOST_PORT:-3306})/${MYSQL_DATABASE:-easygoadmin.goframe.elevue}?charset=utf8mb4&parseTime=true&loc=Local"
|
||||||
GF_GENV_REDIS_DEFAULT_ADDRESS: "${REDIS_HOST:-redis}:${REDIS_HOST_PORT:-6379}"
|
GF_REDIS_DEFAULT_ADDRESS: "${REDIS_HOST:-redis}:${REDIS_HOST_PORT:-6379}"
|
||||||
GF_GENV_REDIS_DEFAULT_PASS: "${REDIS_PASSWORD:-123456}"
|
GF_REDIS_DEFAULT_PASS: "${REDIS_PASSWORD:-123456}"
|
||||||
GF_GENV_FILE_UPLOADDIR: "/data/upload"
|
GF_FILE_UPLOADDIR: "/data/upload"
|
||||||
GF_GENV_JWT_SECRET: "${JWT_SECRET:-please-change-this-jwt-secret}"
|
GF_JWT_SECRET: "${JWT_SECRET:-please-change-this-jwt-secret}"
|
||||||
GF_GENV_CAPTCHA_BYPASSCODE: "" # 生产环境清空万能验证码
|
GF_CAPTCHA_BYPASSCODE: "" # 生产环境清空万能验证码
|
||||||
GF_GENV_APP_ERRORMSG: "false" # 生产环境关闭详细错误信息
|
GF_APP_ERRORMSG: "false" # 生产环境关闭详细错误信息
|
||||||
volumes:
|
volumes:
|
||||||
# 上传文件持久化(两段式上传的 temp 与正式目录都在 uploadDir 下)
|
# 上传文件持久化(两段式上传的 temp 与正式目录都在 uploadDir 下)
|
||||||
- ../upload:/data/upload
|
- ../upload:/data/upload
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import (
|
|||||||
"easygoadmin/internal/logic"
|
"easygoadmin/internal/logic"
|
||||||
"easygoadmin/internal/router"
|
"easygoadmin/internal/router"
|
||||||
"easygoadmin/utility/cache"
|
"easygoadmin/utility/cache"
|
||||||
|
"easygoadmin/utility/config"
|
||||||
|
|
||||||
// 空白导入:触发定时任务处理器 init 自注册
|
// 空白导入:触发定时任务处理器 init 自注册
|
||||||
_ "easygoadmin/internal/task"
|
_ "easygoadmin/internal/task"
|
||||||
@@ -64,6 +65,10 @@ import (
|
|||||||
// 3. 注册中间件链与全部路由
|
// 3. 注册中间件链与全部路由
|
||||||
// 4. 阻塞运行 HTTP 服务
|
// 4. 阻塞运行 HTTP 服务
|
||||||
func Run(ctx context.Context) {
|
func Run(ctx context.Context) {
|
||||||
|
// 配置环境变量覆盖接线(GF_ 前缀环境变量 > 配置文件,12-Factor 优先级)
|
||||||
|
// 须最先执行:缓存/数据库/Redis 初始化都依赖配置读取
|
||||||
|
config.RegisterEnvOverride()
|
||||||
|
|
||||||
// 缓存初始化(JWT黑名单/验证码/字典/身份缓存)
|
// 缓存初始化(JWT黑名单/验证码/字典/身份缓存)
|
||||||
cache.Init(ctx)
|
cache.Init(ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# ======================================================================
|
# ======================================================================
|
||||||
# EasyGoAdmin 运行时配置
|
# EasyGoAdmin 运行时配置
|
||||||
# 环境变量覆盖规则:使用 GF_GENV_ 前缀,例如 GF_GENV_DATABASE_DEFAULT_LINK
|
# 环境变量覆盖规则(12-Factor:环境变量 > 配置文件):
|
||||||
|
# 键名 = 配置路径大写、点换下划线并加 GF_ 前缀,
|
||||||
|
# 例如 database.default.link → GF_DATABASE_DEFAULT_LINK;
|
||||||
|
# 覆盖经 utility/config 在启动时接线(RegisterEnvOverride),
|
||||||
|
# 仅覆盖本文件中已存在的键,空值视为未设置
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Package config 提供配置读取的环境变量覆盖接线
|
||||||
|
//
|
||||||
|
// 覆盖规则遵循 12-Factor 标准优先级:GF_ 前缀环境变量 > 配置文件。
|
||||||
|
// 键名规则:配置路径大写、点换下划线并加 GF_ 前缀
|
||||||
|
// (database.default.link → GF_DATABASE_DEFAULT_LINK)。
|
||||||
|
// 实现方式为包装默认文件适配器(gcfg.Adapter 是框架官方扩展点),
|
||||||
|
// 注册后全部 Get/MustGet 读取点零改动地获得覆盖能力。
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/os/gcfg"
|
||||||
|
"github.com/gogf/gf/v2/os/genv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// adapterEnvFirst 环境变量优先的配置适配器
|
||||||
|
//
|
||||||
|
// 仅覆盖配置文件中已存在的键:环境变量名由配置路径正向推导(无歧义),
|
||||||
|
// 而反向拆分下划线无法还原 uploadDir 这类驼峰键名,故不做补键注入。
|
||||||
|
// 文件返回的 map 会被就地叠加环境变量值,进程内环境恒定,重复叠加幂等。
|
||||||
|
type adapterEnvFirst struct {
|
||||||
|
base gcfg.Adapter // 默认文件适配器,环境未命中时回落
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterEnvOverride 注册环境变量优先的配置适配器(包装并替换默认文件适配器)
|
||||||
|
//
|
||||||
|
// 须在首次读取配置之前调用(服务启动入口最前,含缓存/数据库/Redis 初始化);
|
||||||
|
// 未设置任何 GF_ 环境变量时行为与默认文件适配器完全一致。
|
||||||
|
func RegisterEnvOverride() {
|
||||||
|
gcfg.Instance().SetAdapter(&adapterEnvFirst{base: gcfg.Instance().GetAdapter()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available 委托文件适配器
|
||||||
|
func (a *adapterEnvFirst) Available(ctx context.Context, resource ...string) bool {
|
||||||
|
return a.base.Available(ctx, resource...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 先查 GF_ 前缀环境变量,未命中回落文件适配器;
|
||||||
|
// 文件返回 map(如 database 整节点)时对已有键逐叶叠加环境变量覆盖
|
||||||
|
func (a *adapterEnvFirst) Get(ctx context.Context, pattern string) (any, error) {
|
||||||
|
if v := envValue(pattern); v != nil {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
value, err := a.base.Get(ctx, pattern)
|
||||||
|
if err != nil || value == nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
if m, ok := value.(map[string]any); ok {
|
||||||
|
overlayMap(m, pattern)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data 返回全量配置并叠加环境变量覆盖
|
||||||
|
func (a *adapterEnvFirst) Data(ctx context.Context) (map[string]any, error) {
|
||||||
|
data, err := a.base.Data(ctx)
|
||||||
|
if err != nil || data == nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
overlayMap(data, "")
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// envValue 按键路径取 GF_ 前缀环境变量;未设置或空值返回 nil(空串视为未设置,
|
||||||
|
// 避免容器编排注入的空占位把配置文件中的有效值清空)
|
||||||
|
func envValue(pattern string) any {
|
||||||
|
if pattern == "" || pattern == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := genv.Get("GF_" + strings.ToUpper(strings.ReplaceAll(pattern, ".", "_")))
|
||||||
|
if v == nil || v.String() == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v.Val()
|
||||||
|
}
|
||||||
|
|
||||||
|
// overlayMap 对 map 中已存在的键递归叠加环境变量覆盖
|
||||||
|
//
|
||||||
|
// prefix 为当前层级的配置路径(顶层为空);仅处理标量覆盖,
|
||||||
|
// 切片/数组不支持环境变量替换(本项目可覆盖键均为标量)
|
||||||
|
func overlayMap(m map[string]any, prefix string) {
|
||||||
|
for k, val := range m {
|
||||||
|
path := k
|
||||||
|
if prefix != "" && prefix != "." {
|
||||||
|
path = prefix + "." + k
|
||||||
|
}
|
||||||
|
if env := envValue(path); env != nil {
|
||||||
|
m[k] = env
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if child, ok := val.(map[string]any); ok {
|
||||||
|
overlayMap(child, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/os/gcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeBase 模拟文件适配器,返回固定配置树
|
||||||
|
type fakeBase struct {
|
||||||
|
data map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBase) Available(ctx context.Context, resource ...string) bool { return true }
|
||||||
|
|
||||||
|
func (f *fakeBase) Get(ctx context.Context, pattern string) (any, error) {
|
||||||
|
if pattern == "" || pattern == "." {
|
||||||
|
return f.data, nil
|
||||||
|
}
|
||||||
|
var cur any = f.data
|
||||||
|
for _, part := range splitPattern(pattern) {
|
||||||
|
m, ok := cur.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
cur = m[part]
|
||||||
|
}
|
||||||
|
return cur, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBase) Data(ctx context.Context) (map[string]any, error) { return f.data, nil }
|
||||||
|
|
||||||
|
func splitPattern(pattern string) []string {
|
||||||
|
var parts []string
|
||||||
|
start := 0
|
||||||
|
for i := 0; i < len(pattern); i++ {
|
||||||
|
if pattern[i] == '.' {
|
||||||
|
parts = append(parts, pattern[start:i])
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(parts, pattern[start:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdapterEnvFirst 覆盖优先级核心行为:
|
||||||
|
// - 设置 GF_ 环境变量 → 覆盖文件值(含 map 整节点逐叶叠加)
|
||||||
|
// - 未设置 → 回落文件值
|
||||||
|
// - 空值环境变量 → 视为未设置,不清空文件值
|
||||||
|
func TestAdapterEnvFirst(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
base := &fakeBase{data: map[string]any{
|
||||||
|
"database": map[string]any{
|
||||||
|
"default": map[string]any{
|
||||||
|
"type": "mysql",
|
||||||
|
"link": "file-link",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"jwt": map[string]any{"secret": "file-secret"},
|
||||||
|
}}
|
||||||
|
a := &adapterEnvFirst{base: base}
|
||||||
|
|
||||||
|
// 1. 未设置环境变量:文件值原样返回
|
||||||
|
if v, _ := a.Get(ctx, "database.default.link"); v != "file-link" {
|
||||||
|
t.Fatalf("未设环境变量时应返回文件值,得到: %v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 标量覆盖
|
||||||
|
os.Setenv("GF_JWT_SECRET", "env-secret")
|
||||||
|
defer os.Unsetenv("GF_JWT_SECRET")
|
||||||
|
if v, _ := a.Get(ctx, "jwt.secret"); v != "env-secret" {
|
||||||
|
t.Fatalf("环境变量应覆盖文件值,得到: %v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. map 整节点逐叶叠加(gdb 读 database 整节点的场景)
|
||||||
|
os.Setenv("GF_DATABASE_DEFAULT_LINK", "env-link")
|
||||||
|
defer os.Unsetenv("GF_DATABASE_DEFAULT_LINK")
|
||||||
|
v, _ := a.Get(ctx, "database")
|
||||||
|
m, ok := v.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("database 节点应返回 map,得到: %T", v)
|
||||||
|
}
|
||||||
|
def := m["default"].(map[string]any)
|
||||||
|
if def["link"] != "env-link" {
|
||||||
|
t.Fatalf("整节点读取时 link 应被环境变量覆盖,得到: %v", def["link"])
|
||||||
|
}
|
||||||
|
if def["type"] != "mysql" {
|
||||||
|
t.Fatalf("未覆盖的键应保持文件值,得到: %v", def["type"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 空值环境变量视为未设置(不清空文件值)
|
||||||
|
os.Setenv("GF_DATABASE_DEFAULT_TYPE", "")
|
||||||
|
defer os.Unsetenv("GF_DATABASE_DEFAULT_TYPE")
|
||||||
|
if v, _ := a.Get(ctx, "database.default.type"); v != "mysql" {
|
||||||
|
t.Fatalf("空环境变量应视为未设置,得到: %v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Data 全量叠加
|
||||||
|
data, _ := a.Data(ctx)
|
||||||
|
defData := data["database"].(map[string]any)["default"].(map[string]any)
|
||||||
|
if defData["link"] != "env-link" {
|
||||||
|
t.Fatalf("Data 应叠加环境变量覆盖,得到: %v", defData["link"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRegisterEnvOverride 注册后 g.Cfg() 读取链路获得覆盖能力
|
||||||
|
func TestRegisterEnvOverride(t *testing.T) {
|
||||||
|
adapter, err := gcfg.NewAdapterContent(`database:
|
||||||
|
default:
|
||||||
|
link: file-link
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("构造内容适配器失败: %v", err)
|
||||||
|
}
|
||||||
|
gcfg.Instance().SetAdapter(adapter)
|
||||||
|
RegisterEnvOverride()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
os.Setenv("GF_DATABASE_DEFAULT_LINK", "env-link")
|
||||||
|
defer os.Unsetenv("GF_DATABASE_DEFAULT_LINK")
|
||||||
|
if v := gcfg.Instance().MustGet(ctx, "database.default.link").String(); v != "env-link" {
|
||||||
|
t.Fatalf("注册后 g.Cfg 读取应获得环境变量覆盖,得到: %v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user