# 技术设计：部署自初始化

## 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

无。设计已明确，可直接实施。
