Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-24-add-object-storage/design.md
huang 45aa7deb87
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
feat: 添加环境变量管理工具和部署配置改版
主要改动:
- 新增交互式环境配置脚本 (scripts/setup-env.sh)
- 新增本地启动快捷脚本 (scripts/run-local.sh)
- 新增环境变量模板文件 (.env.example)
- 部署模式改版:使用嵌入式配置 + 环境变量覆盖
- 添加对象存储功能支持
- 改进 IoT 卡片导入任务
- 优化 OpenAPI 文档生成
- 删除旧的配置文件,改用嵌入式默认配置
2026-01-26 10:28:29 +08:00

6.8 KiB
Raw Blame History

通用对象存储 - 技术设计

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

理由

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

备选方案

  • AWS SDK v2API 更现代,但联通云文档无示例,兼容性未知
  • MinIO Go Client功能更丰富但增加额外依赖

Decision 2: Provider 接口设计

选择:定义简洁的 Provider 接口

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: 配置结构

选择:嵌套配置,支持多种预签名有效期

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 封装业务逻辑

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_IDOSS_SECRET_ACCESS_KEY
  2. 数据库迁移

    • 执行迁移添加 storage_bucketstorage_key 字段
  3. 代码部署

    • 部署新版本后端代码
  4. 前端适配

    • 前端发布新版本,使用新的上传流程

回滚策略

  • 数据库字段为可空,不影响回滚
  • 旧版前端可继续使用(需保留旧接口一段时间,或不回滚)

Open Questions

  1. 是否需要文件大小限制?

    • 建议CSV 文件限制 10MB
    • 待确认:具体限制值
  2. 是否需要文件类型校验?

    • 建议:只允许 .csv 文件
    • 待确认:是否需要更严格的校验
  3. 旧接口保留多久?

    • 建议:不保留,直接切换
    • 待确认:与前端团队协调