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,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
无。设计已明确,可直接实施。