feat: 添加环境变量管理工具和部署配置改版
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s

主要改动:
- 新增交互式环境配置脚本 (scripts/setup-env.sh)
- 新增本地启动快捷脚本 (scripts/run-local.sh)
- 新增环境变量模板文件 (.env.example)
- 部署模式改版:使用嵌入式配置 + 环境变量覆盖
- 添加对象存储功能支持
- 改进 IoT 卡片导入任务
- 优化 OpenAPI 文档生成
- 删除旧的配置文件,改用嵌入式默认配置
This commit is contained in:
2026-01-26 10:28:29 +08:00
parent 194078674a
commit 45aa7deb87
94 changed files with 6532 additions and 1967 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-24

View File

@@ -0,0 +1,281 @@
# 技术设计:部署自初始化
## Context
### 当前状态
1. **目录创建分散**
- `pkg/storage/s3.go` 在初始化时创建临时目录
- 日志目录依赖 Dockerfile 预创建
- 没有统一的初始化入口
2. **配置管理复杂**
- 4 个外部配置文件:`config.yaml``config.dev.yaml``config.staging.yaml``config.prod.yaml`
- `pkg/config/watcher.go` 实现热重载(开发阶段不需要)
- 必须手动拷贝配置文件才能启动
3. **部署流程繁琐**
- 需要手动创建目录结构
- 需要手动拷贝配置文件
- Docker Compose 挂载 configs 目录
### 约束
- 使用 Go 1.16+ 的 `go:embed` 特性
- 保持 Viper 作为配置解析库
- 容器内以非 root 用户 (appuser:1000) 运行
## Goals / Non-Goals
**Goals:**
- 应用启动时自动创建所有必需目录
- 配置嵌入二进制,无需外部配置文件
- 环境变量作为配置覆盖机制
- 部署流程简化到 1 步
**Non-Goals:**
- 配置热重载(开发阶段移除)
- 多配置文件支持(统一用嵌入默认值 + 环境变量)
- 向后兼容旧的配置方式
## Decisions
### Decision 1: 目录初始化放在应用层
**选择**: 在 `main.go` 启动时调用集中化的目录初始化函数
**备选方案**:
| 方案 | 优点 | 缺点 |
|------|------|------|
| Dockerfile RUN mkdir | 镜像固定 | 不支持运行时配置路径 |
| Entrypoint 脚本 | 运行时动态 | Shell 脚本维护成本高 |
| **应用代码初始化** | 跨环境通用、错误处理清晰 | 无 |
**理由**: 应用代码可以读取配置中的路径,提供 Go 级别的错误处理并在所有部署环境Docker、K8s、裸机通用。
### Decision 2: 配置嵌入 + 环境变量覆盖
**选择**: 使用 `go:embed` 嵌入默认配置,环境变量覆盖
**配置优先级**:
```
环境变量 (JUNHONG_*) > 嵌入默认值
```
**备选方案**:
| 方案 | 优点 | 缺点 |
|------|------|------|
| 纯外部文件 | 运行时可改 | 部署复杂 |
| 纯环境变量 | 12-Factor | 复杂配置难表达 |
| **嵌入 + 环境变量** | 开箱即用 + 灵活覆盖 | 改默认值需重编译 |
**理由**: 嵌入配置保证"开箱即用",环境变量覆盖满足生产环境定制需求。开发阶段不需要频繁改默认值。
### Decision 3: 环境变量命名规范
**选择**: `JUNHONG_` 前缀 + 下划线分隔
**格式**: `JUNHONG_{SECTION}_{KEY}`
**示例**:
```bash
JUNHONG_DATABASE_HOST=localhost
JUNHONG_DATABASE_PORT=5432
JUNHONG_REDIS_ADDRESS=localhost:6379
JUNHONG_SERVER_ADDRESS=:3000
```
**理由**:
- 前缀避免与系统环境变量冲突
- 下划线分隔便于 Viper 的 `SetEnvKeyReplacer` 处理
- 符合业界惯例(类似 `POSTGRES_``REDIS_` 等)
### Decision 4: 删除配置热重载
**选择**: 删除 `pkg/config/watcher.go` 及相关逻辑
**理由**:
- 开发阶段,配置变更频率低
- 重启容器即可应用新配置
- 减少代码复杂度
### Decision 5: 目录降级策略
**选择**: 权限不足时使用系统临时目录作为 fallback
```go
if err := os.MkdirAll(dir, 0755); err != nil {
if os.IsPermission(err) {
fallback := filepath.Join(os.TempDir(), "junhong", filepath.Base(dir))
os.MkdirAll(fallback, 0755)
return fallback, nil
}
return "", err
}
```
**理由**: 提高容错性,即使权限配置不当也能启动(降级运行)。
## Architecture
### 启动流程
```
main.go
├── 1. config.Load() // 加载嵌入配置 + 环境变量覆盖
│ ├── 读取 go:embed 的 defaults/config.yaml
│ ├── 应用 JUNHONG_* 环境变量覆盖
│ └── 验证必填配置
├── 2. bootstrap.EnsureDirectories(cfg) // 创建所有必需目录
│ ├── 临时文件目录
│ ├── 日志目录
│ └── 其他运行时目录
├── 3. logger.Init(cfg) // 初始化日志
├── 4. database.Init(cfg) // 初始化数据库
└── 5. ... 其他组件初始化
```
### 目录结构
```
pkg/
├── bootstrap/
│ └── directories.go # 新增:目录初始化
├── config/
│ ├── config.go # 保留:配置结构定义
│ ├── loader.go # 重写:嵌入配置加载
│ ├── embedded.go # 新增go:embed 逻辑
│ ├── defaults/
│ │ └── config.yaml # 新增:嵌入的默认配置
│ └── watcher.go # 删除
```
### 嵌入配置内容
```yaml
# pkg/config/defaults/config.yaml
server:
address: ":3000"
read_timeout: 30s
write_timeout: 30s
shutdown_timeout: 30s
prefork: false
database:
host: "" # 必须通过 JUNHONG_DATABASE_HOST 设置
port: 5432
user: "" # 必须通过 JUNHONG_DATABASE_USER 设置
password: "" # 必须通过 JUNHONG_DATABASE_PASSWORD 设置
dbname: "" # 必须通过 JUNHONG_DATABASE_DBNAME 设置
sslmode: "disable"
max_open_conns: 25
max_idle_conns: 10
conn_max_lifetime: 5m
redis:
address: "" # 必须通过 JUNHONG_REDIS_ADDRESS 设置
port: 6379
password: ""
db: 0
pool_size: 10
min_idle_conns: 5
dial_timeout: 5s
read_timeout: 3s
write_timeout: 3s
storage:
provider: "s3"
temp_dir: "/tmp/junhong-storage"
s3:
endpoint: ""
region: ""
bucket: ""
access_key_id: ""
secret_access_key: ""
use_ssl: false
path_style: true
presign:
upload_expires: 15m
download_expires: 24h
logging:
level: "info"
development: false
app_log:
filename: "/app/logs/app.log"
max_size: 100
max_backups: 3
max_age: 7
compress: true
access_log:
filename: "/app/logs/access.log"
max_size: 100
max_backups: 3
max_age: 7
compress: true
queue:
concurrency: 10
retry_max: 5
timeout: 10m
jwt:
secret_key: "" # 必须通过 JUNHONG_JWT_SECRET_KEY 设置
token_duration: 24h
access_token_ttl: 24h
refresh_token_ttl: 168h
middleware:
enable_rate_limiter: false
rate_limiter:
max: 100
expiration: 1m
storage: "memory"
```
## Risks / Trade-offs
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 嵌入配置修改需重新编译 | 低(开发阶段) | 敏感配置通过环境变量,嵌入的只是默认值 |
| 环境变量泄露 | 中 | 使用 K8s Secrets 或 Docker Secrets 管理 |
| 目录降级后行为不一致 | 低 | 降级时打印 WARN 日志,明确告知 |
| 删除热重载后调试不便 | 低 | 开发时直接重启进程,生产用 rolling update |
## Migration Plan
### 实施步骤
1. **新增目录初始化模块**
- 创建 `pkg/bootstrap/directories.go`
- 在 main.go 中调用
2. **新增配置嵌入模块**
- 创建 `pkg/config/defaults/config.yaml`
- 创建 `pkg/config/embedded.go`
- 重写 `pkg/config/loader.go`
3. **清理旧配置逻辑**
- 删除 `pkg/config/watcher.go`
- 删除 `configs/*.yaml`
4. **更新 Docker 配置**
- 更新 `Dockerfile.api``Dockerfile.worker`
- 重写 `docker-compose.prod.yml`
5. **更新文档**
- 更新 README.md 部署说明
- 更新环境变量文档
### 回滚策略
如需回滚,恢复 git 提交即可(开发阶段无生产数据风险)。
## Open Questions
无。设计已明确,可直接实施。

View File

@@ -0,0 +1,104 @@
# 提案:部署自初始化
## Why
当前应用部署需要手动创建目录结构、拷贝配置文件,过程繁琐且容易出错。临时目录创建逻辑分散在各组件中,非 root 用户可能因权限问题导致启动失败。配置文件必须外部提供,无法实现"开箱即用"的部署体验。
本变更旨在实现**应用自初始化**,让应用启动时自动创建所需目录、使用嵌入的默认配置,大幅简化部署流程。
## What Changes
### 1. 集中化目录初始化
- 新增 `pkg/bootstrap/directories.go`,在应用启动时统一创建所有必需目录
- 移除各组件(如 `s3.go`)中分散的目录创建逻辑
- 提供降级策略:权限不足时自动使用备用路径
### 2. 配置嵌入机制
- 使用 `go:embed` 将默认配置嵌入二进制文件
- 配置优先级:**环境变量 > 嵌入默认值**(移除外部配置文件依赖)
- 敏感配置(数据库密码等)通过环境变量提供
- **移除** configs/ 目录和配置文件热重载机制(开发阶段不需要)
### 3. Docker 部署简化
- Dockerfile 预创建关键目录并设置正确权限
- **移除** configs 目录挂载,全部使用环境变量
- docker-compose 只挂载日志目录(持久化需求)
### 4. 环境变量规范化
- 统一环境变量前缀:`JUNHONG_`
- 支持嵌套配置:`JUNHONG_DATABASE_HOST``JUNHONG_REDIS_ADDRESS`
- 配置验证:必填配置未设置时启动失败并给出明确提示
### 5. 清理冗余
- **删除** `pkg/config/watcher.go`(配置热重载)
- **删除** `configs/*.yaml` 外部配置文件
- **删除** docker-compose 中的 configs 卷挂载
- **简化** `pkg/config/loader.go`
## Capabilities
### New Capabilities
- `bootstrap-init`: 应用启动时的集中化初始化机制,包括目录创建、配置加载、验证等
- `embedded-config`: 配置嵌入机制,使用 go:embed + 环境变量覆盖,无需外部配置文件
### Modified Capabilities
- `dependency-injection`: 调整 bootstrap 流程,在组件初始化前完成目录和配置的准备
## Impact
### 代码变更
| 文件/目录 | 变更类型 | 说明 |
|-----------|----------|------|
| `pkg/bootstrap/directories.go` | 新增 | 目录初始化逻辑 |
| `pkg/config/embedded.go` | 新增 | 配置嵌入和加载逻辑 |
| `pkg/config/defaults/config.yaml` | 新增 | 嵌入的默认配置文件 |
| `pkg/config/loader.go` | 重写 | 简化为嵌入配置 + 环境变量 |
| `pkg/config/watcher.go` | **删除** | 不再需要热重载 |
| `pkg/storage/s3.go` | 修改 | 移除目录创建逻辑 |
| `cmd/api/main.go` | 修改 | 调用目录初始化 |
| `cmd/worker/main.go` | 修改 | 调用目录初始化 |
| `internal/bootstrap/` | 修改 | 调整初始化顺序 |
| `configs/*.yaml` | **删除** | 配置嵌入后不再需要 |
### Docker 变更
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `Dockerfile.api` | 修改 | 预创建目录、删除 COPY configs |
| `Dockerfile.worker` | 修改 | 预创建目录、删除 COPY configs |
| `docker-compose.prod.yml` | 重写 | 纯环境变量配置、删除 configs 挂载 |
| `docker/entrypoint-api.sh` | 简化 | 只保留迁移逻辑 |
### 部署流程变更
**变更前**5 步):
```bash
# 1. SSH 到服务器
# 2. 创建目录 mkdir -p /opt/junhong_cmp/{configs,logs}
# 3. 复制 docker-compose.prod.yml
# 4. 复制配置文件到 configs/
# 5. docker-compose up -d
```
**变更后**1 步):
```bash
# docker-compose up -dCI/CD 自动部署,或手动拉取 compose 文件)
```
### 依赖
- Go 1.16+`go:embed` 支持)
- 无新增外部依赖
## 预期收益
| 指标 | 变更前 | 变更后 |
|------|--------|--------|
| 首次部署步骤 | 5 步 | 1 步 |
| 配置文件 | 4 个外部文件 | 0 个(嵌入) |
| 权限失败风险 | 高 | 低(降级策略) |
| 环境可移植性 | Docker only | Docker/K8s/裸机 |
| 配置热重载 | 支持 | 移除(开发阶段不需要) |

View File

@@ -0,0 +1,78 @@
# bootstrap-init 规范
应用启动时的集中化初始化机制,确保所有必需目录在组件初始化前创建完成。
## ADDED Requirements
### Requirement: 集中化目录初始化
系统 SHALL 在应用启动时通过 `bootstrap.EnsureDirectories()` 函数统一创建所有必需的运行时目录。
目录列表:
- 临时文件目录(从 `config.Storage.TempDir` 读取)
- 应用日志目录(从 `config.Logging.AppLog.Filename` 提取目录部分)
- 访问日志目录(从 `config.Logging.AccessLog.Filename` 提取目录部分)
#### Scenario: 成功创建所有目录
- **WHEN** 应用启动且所有目录路径可写
- **THEN** 系统创建所有必需目录,权限为 0755
- **AND** 函数返回 nil
#### Scenario: 目录已存在
- **WHEN** 应用启动且目录已存在
- **THEN** 系统跳过创建,不报错
- **AND** 函数返回 nil
#### Scenario: 配置路径为空
- **WHEN** 某个目录配置为空字符串
- **THEN** 系统跳过该目录的创建
- **AND** 不影响其他目录的创建
### Requirement: 权限降级策略
系统 SHALL 在目录创建权限不足时自动降级到系统临时目录。
#### Scenario: 权限不足时降级
- **WHEN** 创建目录因权限不足失败os.IsPermission 为 true
- **THEN** 系统使用 `os.TempDir()/junhong/<原目录名>` 作为降级路径
- **AND** 记录 WARN 级别日志,包含原路径和降级路径
- **AND** 函数返回降级后的路径
#### Scenario: 非权限错误
- **WHEN** 创建目录失败且不是权限问题
- **THEN** 系统返回错误,应用启动失败
- **AND** 错误信息包含目录路径和原始错误
### Requirement: 初始化顺序
系统 SHALL 确保目录初始化在所有组件初始化之前完成。
#### Scenario: 正确的初始化顺序
- **WHEN** 应用启动
- **THEN** 执行顺序为:
1. config.Load() 加载配置
2. bootstrap.EnsureDirectories() 创建目录
3. logger.Init() 初始化日志
4. 其他组件初始化
#### Scenario: 目录初始化失败
- **WHEN** `bootstrap.EnsureDirectories()` 返回错误
- **THEN** 应用立即退出,不继续初始化其他组件
- **AND** 错误信息输出到 stderr
### Requirement: 移除分散的目录创建逻辑
系统 SHALL 移除各组件中分散的目录创建代码。
#### Scenario: S3Provider 不再创建目录
- **WHEN** 初始化 S3Provider
- **THEN** 不再调用 `os.MkdirAll` 创建临时目录
- **AND** 假设目录已由 bootstrap 创建

View File

@@ -0,0 +1,133 @@
# embedded-config 规范
配置嵌入机制,使用 go:embed 将默认配置嵌入二进制文件,通过环境变量覆盖。
## ADDED Requirements
### Requirement: 配置嵌入
系统 SHALL 使用 Go 的 `go:embed` 指令将默认配置文件嵌入二进制文件。
嵌入文件位置:`pkg/config/defaults/config.yaml`
#### Scenario: 加载嵌入配置
- **WHEN** 调用 `config.Load()`
- **THEN** 系统从嵌入的 `defaults/config.yaml` 读取默认配置
- **AND** 无需外部配置文件即可启动
#### Scenario: 嵌入配置包含完整结构
- **WHEN** 读取嵌入配置
- **THEN** 配置包含所有配置节server、database、redis、storage、logging、queue、jwt、middleware
### Requirement: 环境变量覆盖
系统 SHALL 支持通过环境变量覆盖嵌入的默认配置值。
环境变量格式:`JUNHONG_{SECTION}_{KEY}`
#### Scenario: 环境变量覆盖配置
- **WHEN** 设置环境变量 `JUNHONG_DATABASE_HOST=myhost`
- **THEN** `config.Database.Host` 的值为 "myhost"
- **AND** 覆盖嵌入配置中的默认值
#### Scenario: 嵌套配置覆盖
- **WHEN** 设置环境变量 `JUNHONG_LOGGING_LEVEL=debug`
- **THEN** `config.Logging.Level` 的值为 "debug"
#### Scenario: 未设置环境变量
- **WHEN** 未设置某个配置的环境变量
- **THEN** 使用嵌入配置中的默认值
### Requirement: 配置优先级
系统 SHALL 按以下优先级应用配置(高到低):
1. 环境变量 (JUNHONG_*)
2. 嵌入默认值 (go:embed)
#### Scenario: 优先级验证
- **WHEN** 嵌入配置中 `server.address` 为 ":3000"
- **AND** 设置环境变量 `JUNHONG_SERVER_ADDRESS=:8080`
- **THEN** 最终 `config.Server.Address` 为 ":8080"
### Requirement: 必填配置验证
系统 SHALL 在加载配置后验证必填配置项是否已设置。
必填配置项:
- `database.host`
- `database.user`
- `database.password`
- `database.dbname`
- `redis.address`
- `jwt.secret_key`
#### Scenario: 必填配置缺失
- **WHEN** 必填配置项为空且未通过环境变量设置
- **THEN** `config.Load()` 返回错误
- **AND** 错误信息明确指出缺失的配置项和对应的环境变量名
#### Scenario: 必填配置通过环境变量提供
- **WHEN** 所有必填配置通过环境变量设置
- **THEN** `config.Load()` 成功返回配置
### Requirement: 删除外部配置文件支持
系统 SHALL 移除对外部配置文件的支持。
#### Scenario: 不读取 configs 目录
- **WHEN** 应用启动
- **THEN** 不读取 `configs/*.yaml` 文件
- **AND** 不依赖 `CONFIG_PATH``CONFIG_ENV` 环境变量
### Requirement: 删除配置热重载
系统 SHALL 移除配置热重载功能。
#### Scenario: 不监听配置文件变化
- **WHEN** 应用运行中
- **THEN** 不使用 fsnotify 监听文件变化
- **AND** 删除 `pkg/config/watcher.go`
#### Scenario: 配置变更需重启
- **WHEN** 需要更改配置
- **THEN** 必须重启应用使新配置生效
### Requirement: 环境变量前缀
系统 SHALL 使用 `JUNHONG_` 作为环境变量前缀。
#### Scenario: 前缀隔离
- **WHEN** 存在环境变量 `DATABASE_HOST=other`
- **AND** 存在环境变量 `JUNHONG_DATABASE_HOST=correct`
- **THEN** `config.Database.Host` 为 "correct"
- **AND** 忽略无前缀的 `DATABASE_HOST`
### Requirement: 敏感配置处理
系统 SHALL 确保敏感配置不嵌入二进制文件。
敏感配置项(嵌入值为空):
- `database.password`
- `redis.password`
- `jwt.secret_key`
- `storage.s3.access_key_id`
- `storage.s3.secret_access_key`
#### Scenario: 敏感配置默认为空
- **WHEN** 读取嵌入配置
- **THEN** 敏感配置项的值为空字符串
- **AND** 必须通过环境变量提供实际值

View File

@@ -0,0 +1,82 @@
# 实施任务清单
## 1. 配置嵌入模块
- [x] 1.1 创建 `pkg/config/defaults/config.yaml` 嵌入配置文件
- [x] 1.2 创建 `pkg/config/embedded.go`,实现 go:embed 加载逻辑
- [x] 1.3 重写 `pkg/config/loader.go`,使用嵌入配置 + 环境变量覆盖
- [x] 1.4 更新 `pkg/config/config.go` 中的 Validate() 方法,添加必填配置验证
- [x] 1.5 删除 `pkg/config/watcher.go` 配置热重载模块
- [x] 1.6 编写配置加载单元测试
## 2. 目录初始化模块
- [x] 2.1 创建 `pkg/bootstrap/directories.go`,实现 EnsureDirectories() 函数
- [x] 2.2 实现权限降级策略(权限不足时使用临时目录)
- [x] 2.3 编写目录初始化单元测试
- [x] 2.4 移除 `pkg/storage/s3.go` 中的目录创建逻辑
## 3. 应用入口改造
- [x] 3.1 更新 `cmd/api/main.go`,在配置加载后调用 bootstrap.EnsureDirectories()
- [x] 3.2 更新 `cmd/worker/main.go`,同样调用目录初始化
- [x] 3.3 调整 `internal/bootstrap/` 中的初始化顺序(无需修改,顺序正确)
## 4. Docker 配置更新
- [x] 4.1 更新 `Dockerfile.api`:预创建目录、移除 COPY configs
- [x] 4.2 更新 `Dockerfile.worker`:预创建目录、移除 COPY configs
- [x] 4.3 重写 `docker-compose.prod.yml`:纯环境变量配置
- [x] 4.4 简化 `docker/entrypoint-api.sh`:移除配置相关逻辑
## 5. 清理旧文件
- [x] 5.1 删除 `configs/config.yaml`
- [x] 5.2 删除 `configs/config.dev.yaml`
- [x] 5.3 删除 `configs/config.staging.yaml`
- [x] 5.4 删除 `configs/config.prod.yaml`
- [x] 5.5 删除 `configs/` 目录(如果为空)
## 6. 文档更新
- [x] 6.1 更新 README.md 部署说明
- [x] 6.2 更新环境变量列表文档(创建 docs/environment-variables.md
- [x] 6.3 更新关键文档auth-usage-guide, object-storage, add-default-admin-init
## 7. 验证
- [x] 7.1 本地运行测试:`go test ./...`config 和 bootstrap 测试通过)
- [x] 7.2 本地 Docker 构建测试API 和 Worker 镜像构建成功)
- [x] 7.3 本地 docker-compose 启动测试(需要外部 PostgreSQL/Redis配置验证通过
- [x] 7.4 验证环境变量覆盖功能TestLoad_EnvOverride 通过)
---
## 实施完成总结
**完成日期**: 2026-01-24
### 主要变更
1. **配置嵌入机制**
- 默认配置嵌入二进制文件 (`pkg/config/defaults/config.yaml`)
- 环境变量覆盖使用 `JUNHONG_` 前缀
- 移除配置热重载功能
2. **目录自动初始化**
- `pkg/bootstrap/directories.go` 实现 `EnsureDirectories()`
- 支持权限降级(无权限时使用临时目录)
3. **Docker 部署简化**
- 移除 `COPY configs` 指令
- 纯环境变量配置
- 更新 `docker-compose.prod.yml``entrypoint-api.sh`
4. **文档更新**
- 创建 `docs/environment-variables.md` 完整环境变量文档
- 更新 README.md 部署说明
- 更新相关功能文档
### 后续工作(可选)
- 更新剩余的旧文档rate-limiting.md, deployment-guide.md 等)