# 通用对象存储 - 技术设计

## Context

### 背景

当前系统的 ICCID 导入功能采用传统的文件上传方式：前端上传 CSV 文件到后端，后端解析后处理。这种方式存在以下问题：

1. **性能瓶颈**：大文件上传占用后端带宽和内存
2. **扩展性差**：未来导出功能也需要文件存储能力
3. **安全风险**：文件处理在内存中进行，存在 OOM 风险

### 现状

- 联通云对象存储（CUCloud OSS）已开通，Bucket `cmp` 已创建
- 联通云 OSS 兼容 AWS S3 API，支持预签名 URL（已验证）
- 项目使用 `github.com/aws/aws-sdk-go` v1 版本

### 约束

- 本项目作为公司后端模板，设计需要通用化
- 联通云 OSS 的 Endpoint 格式：`http://obs-{region}.cucloud.cn`
- 同一时刻只使用一个云存储提供商（不需要多云并存）

## Goals / Non-Goals

### Goals

1. **通用对象存储能力**：提供可复用的对象存储包 `pkg/storage/`
2. **预签名 URL 支持**：前端直传，不经过后端
3. **ICCID 导入改造**：集成对象存储，提升性能
4. **配置驱动**：通过配置文件切换不同云存储

### Non-Goals

1. **不实现导出功能**：只准备能力，导出功能后续单独开发
2. **不实现多云并存**：同一时刻只用一个云
3. **不删除对象存储文件**：导入完成后只删除本地临时文件
4. **不实现断点续传**：小文件（CSV）不需要

## Decisions

### Decision 1: 使用 AWS SDK v1 而非 v2

**选择**：`github.com/aws/aws-sdk-go`（v1）

**理由**：
- 联通云官方文档推荐使用 v1
- 已验证 v1 在联通云上的预签名功能正常工作
- v1 的 API 更简洁，学习成本低

**备选方案**：
- AWS SDK v2：API 更现代，但联通云文档无示例，兼容性未知
- MinIO Go Client：功能更丰富，但增加额外依赖

### Decision 2: Provider 接口设计

**选择**：定义简洁的 `Provider` 接口

```go
type Provider interface {
    // 上传文件
    Upload(ctx context.Context, key string, reader io.Reader, contentType string) error
    // 下载文件到 io.Writer
    Download(ctx context.Context, key string, writer io.Writer) error
    // 下载文件到本地临时文件
    DownloadToTemp(ctx context.Context, key string) (localPath string, cleanup func(), err error)
    // 删除文件
    Delete(ctx context.Context, key string) error
    // 检查文件是否存在
    Exists(ctx context.Context, key string) (bool, error)
    // 生成上传预签名 URL
    GetUploadURL(ctx context.Context, key string, contentType string, expires time.Duration) (string, error)
    // 生成下载预签名 URL
    GetDownloadURL(ctx context.Context, key string, expires time.Duration) (string, error)
}
```

**理由**：
- 接口方法覆盖导入导出所需的全部操作
- `DownloadToTemp` 封装临时文件管理，调用者无需关心清理
- 不暴露 Bucket 参数，由实现内部管理（配置驱动）

### Decision 3: 文件路径规范

**选择**：`{purpose}/{year}/{month}/{day}/{uuid}.{ext}`

```
imports/2025/01/24/550e8400-e29b-41d4.csv
exports/2025/01/24/123456-cards.xlsx
attachments/2025/01/24/license.pdf
```

**理由**：
- 按日期组织便于管理和清理
- UUID 保证唯一性
- purpose 前缀区分业务场景

### Decision 4: 配置结构

**选择**：嵌套配置，支持多种预签名有效期

```yaml
storage:
  provider: "s3"
  s3:
    endpoint: "http://obs-helf.cucloud.cn"
    region: "cn-langfang-2"
    bucket: "cmp"
    access_key_id: "${OSS_ACCESS_KEY_ID}"
    secret_access_key: "${OSS_SECRET_ACCESS_KEY}"
    use_ssl: false
    path_style: true
  presign:
    upload_expires: "15m"
    download_expires: "24h"
  temp_dir: "/tmp/junhong-storage"
```

**理由**：
- 凭证通过环境变量注入，不硬编码
- 预签名有效期可配置，适应不同场景
- `path_style: true` 确保联通云兼容性

### Decision 5: Service 层封装

**选择**：创建 `StorageService` 封装业务逻辑

```go
type StorageService struct {
    provider Provider
    config   *config.StorageConfig
}

// 获取上传 URL（自动生成 file_key）
func (s *StorageService) GetUploadURL(ctx context.Context, purpose, fileName string) (*PresignResult, error)

// 下载到临时文件（自动清理）
func (s *StorageService) DownloadToTemp(ctx context.Context, fileKey string) (string, func(), error)
```

**理由**：
- 封装 file_key 生成逻辑（日期 + UUID）
- 统一管理临时文件清理
- Handler 层只关心业务参数

### Decision 6: 导入接口改造

**选择**：移除文件上传，改为传递 file_key

**Before**:
```
POST /api/admin/iot-cards/import
Content-Type: multipart/form-data
carrier_id, batch_no, file
```

**After**:
```
POST /api/admin/iot-cards/import
Content-Type: application/json
{
  "carrier_id": 1,
  "batch_no": "BATCH-2025-01",
  "file_key": "imports/2025/01/24/abc123.csv"
}
```

**理由**：
- JSON 接口更简洁
- 文件已在对象存储，只需传路径
- Worker 从对象存储下载处理

## Risks / Trade-offs

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 联通云服务不可用 | 无法上传/下载文件 | 1) 配置超时和重试 2) 监控告警 |
| 预签名 URL 泄露 | 文件可能被非法访问 | 1) 短有效期（15分钟） 2) 使用 HTTPS |
| 临时文件未清理 | 磁盘空间占用 | 1) defer cleanup() 2) 定期清理任务 |
| **BREAKING** 接口变更 | 前端需要适配 | 1) 与前端团队同步 2) 提供迁移文档 |

## 包结构

```
pkg/storage/
├── storage.go          # Provider 接口定义
├── types.go            # 公共类型（PresignResult, Config）
├── s3.go               # S3 兼容实现
└── service.go          # StorageService 封装

internal/service/storage/
└── service.go          # 业务层 Service（可选，如需更多业务逻辑）

internal/handler/admin/
└── storage.go          # StorageHandler（获取上传 URL）
```

## Migration Plan

### 部署步骤

1. **配置准备**：
   - 在各环境配置文件中添加 `storage` 配置块
   - 设置环境变量 `OSS_ACCESS_KEY_ID` 和 `OSS_SECRET_ACCESS_KEY`

2. **数据库迁移**：
   - 执行迁移添加 `storage_bucket`、`storage_key` 字段

3. **代码部署**：
   - 部署新版本后端代码

4. **前端适配**：
   - 前端发布新版本，使用新的上传流程

### 回滚策略

- 数据库字段为可空，不影响回滚
- 旧版前端可继续使用（需保留旧接口一段时间，或不回滚）

## Open Questions

1. **是否需要文件大小限制？**
   - 建议：CSV 文件限制 10MB
   - 待确认：具体限制值

2. **是否需要文件类型校验？**
   - 建议：只允许 `.csv` 文件
   - 待确认：是否需要更严格的校验

3. **旧接口保留多久？**
   - 建议：不保留，直接切换
   - 待确认：与前端团队协调
