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,233 @@
# 通用对象存储 - 技术设计
## 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 v2API 更现代,但联通云文档无示例,兼容性未知
- 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. **旧接口保留多久?**
- 建议:不保留,直接切换
- 待确认:与前端团队协调

View File

@@ -0,0 +1,97 @@
# 通用对象存储能力
## Why
当前 ICCID 导入功能通过后端接收上传文件占用服务器带宽和内存大文件处理效率低。同时未来的导出功能也需要文件存储能力。需要接入联通云对象存储S3 兼容),采用预签名 URL 方案实现前端直传,提升性能和安全性。
此外,本项目作为公司后端模板项目,对象存储应设计为通用能力,方便复用到其他系统。
## What Changes
- **新增通用对象存储包**`pkg/storage/` 提供 S3 兼容的对象存储能力
- 支持上传、下载、删除、检查存在性
- 支持生成预签名上传/下载 URL
- 可扩展支持多云(阿里云、腾讯云等,仅需更换 Endpoint
- **新增存储 API 接口**:供前端获取预签名上传 URL
- `POST /api/admin/storage/upload-url`:获取上传预签名 URL
- **改造 ICCID 导入流程**
- 移除原有的文件上传处理(`c.FormFile`
- 改为接收 `file_key` 参数(对象存储路径)
- Worker 从对象存储下载文件后处理
- 处理完成后删除本地临时文件
- **数据模型变更**
- `IotCardImportTask` 新增 `storage_bucket``storage_key` 字段
- 需要数据库迁移
- **配置结构扩展**
- 新增 `storage` 配置块endpoint、region、bucket、credentials
## Capabilities
### New Capabilities
- `object-storage`: 通用对象存储能力,提供 S3 兼容的文件上传、下载、删除、预签名 URL 生成功能
### Modified Capabilities
- `iot-card-import`: ICCID 导入流程改造,从直接上传改为对象存储集成
## Impact
### 代码变更
| 层级 | 变更内容 |
|------|----------|
| `pkg/storage/` | 新增Provider 接口、S3 实现、配置类型 |
| `pkg/config/` | 修改:新增 StorageConfig 结构 |
| `configs/` | 修改:新增 storage 配置块 |
| `internal/bootstrap/` | 修改:初始化 Storage Provider |
| `internal/handler/admin/` | 新增StorageHandler获取上传 URL|
| `internal/handler/admin/` | 修改IotCardImportHandler移除文件上传|
| `internal/service/iot_card_import/` | 修改:接收 file_key 而非文件流 |
| `internal/task/` | 修改:从对象存储下载文件处理 |
| `internal/model/` | 修改IotCardImportTask 新增字段 |
| `internal/routes/` | 修改:新增 storage 路由 |
| `migrations/` | 新增:添加 storage 字段迁移 |
### API 变更
| 接口 | 变更类型 | 说明 |
|------|----------|------|
| `POST /api/admin/storage/upload-url` | 新增 | 获取预签名上传 URL |
| `POST /api/admin/iot-cards/import` | **BREAKING** | 移除文件上传,改为传 file_key |
### 依赖变更
| 依赖 | 说明 |
|------|------|
| `github.com/aws/aws-sdk-go` | 新增AWS S3 兼容 SDK已验证联通云支持 |
### 配置变更
```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" # 临时文件目录
```
### 前端适配
前端需要配合修改上传流程:
1. 先调用 `POST /api/admin/storage/upload-url` 获取预签名 URL
2. 直接 PUT 到预签名 URL 上传文件
3. 上传成功后调用导入接口,传入 `file_key`

View File

@@ -0,0 +1,217 @@
# 对象存储能力规格
## ADDED Requirements
### Requirement: Provider 接口
系统 SHALL 提供统一的对象存储 Provider 接口,支持 S3 兼容的对象存储服务。
接口定义:
```go
type Provider interface {
Upload(ctx context.Context, key string, reader io.Reader, contentType string) error
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)
GetUploadURL(ctx context.Context, key string, contentType string, expires time.Duration) (string, error)
GetDownloadURL(ctx context.Context, key string, expires time.Duration) (string, error)
}
```
#### Scenario: 创建 S3 Provider
- **WHEN** 系统启动时读取 storage 配置
- **THEN** 系统 SHALL 创建 S3Provider 实例并验证连接
#### Scenario: 配置缺失
- **WHEN** storage 配置未设置或不完整
- **THEN** 系统 SHALL 记录警告日志并跳过初始化(不影响启动)
---
### Requirement: 文件上传
系统 SHALL 支持通过 Provider 接口上传文件到对象存储。
#### Scenario: 上传成功
- **WHEN** 调用 `Upload(ctx, "imports/test.csv", reader, "text/csv")`
- **THEN** 文件 SHALL 被上传到配置的 Bucket 中指定路径
- **THEN** 方法 SHALL 返回 nil
#### Scenario: 上传失败
- **WHEN** 对象存储服务不可用
- **THEN** 方法 SHALL 返回包含错误详情的 error
---
### Requirement: 文件下载
系统 SHALL 支持从对象存储下载文件。
#### Scenario: 下载到 Writer
- **WHEN** 调用 `Download(ctx, "imports/test.csv", writer)`
- **THEN** 文件内容 SHALL 被写入到提供的 writer
#### Scenario: 下载到临时文件
- **WHEN** 调用 `DownloadToTemp(ctx, "imports/test.csv")`
- **THEN** 系统 SHALL 下载文件到临时目录
- **THEN** 方法 SHALL 返回本地文件路径和 cleanup 函数
- **THEN** 调用 cleanup() 后临时文件 SHALL 被删除
#### Scenario: 文件不存在
- **WHEN** 下载的文件在对象存储中不存在
- **THEN** 方法 SHALL 返回 "文件不存在" 错误
---
### Requirement: 文件删除
系统 SHALL 支持从对象存储删除文件。
#### Scenario: 删除成功
- **WHEN** 调用 `Delete(ctx, "imports/test.csv")`
- **THEN** 文件 SHALL 从对象存储中删除
- **THEN** 方法 SHALL 返回 nil
#### Scenario: 删除不存在的文件
- **WHEN** 删除的文件不存在
- **THEN** 方法 SHALL 返回 nil幂等操作
---
### Requirement: 文件存在性检查
系统 SHALL 支持检查文件是否存在于对象存储。
#### Scenario: 文件存在
- **WHEN** 调用 `Exists(ctx, "imports/test.csv")` 且文件存在
- **THEN** 方法 SHALL 返回 (true, nil)
#### Scenario: 文件不存在
- **WHEN** 调用 `Exists(ctx, "imports/test.csv")` 且文件不存在
- **THEN** 方法 SHALL 返回 (false, nil)
---
### Requirement: 预签名上传 URL
系统 SHALL 支持生成预签名上传 URL允许前端直接上传文件到对象存储。
#### Scenario: 生成上传 URL
- **WHEN** 调用 `GetUploadURL(ctx, "imports/test.csv", "text/csv", 15*time.Minute)`
- **THEN** 方法 SHALL 返回有效的预签名 URL
- **THEN** URL SHALL 在指定时间15分钟后过期
- **THEN** 使用该 URL 的 PUT 请求 SHALL 能成功上传文件
#### Scenario: URL 过期后
- **WHEN** 使用过期的预签名 URL 上传
- **THEN** 对象存储 SHALL 返回 403 Forbidden
---
### Requirement: 预签名下载 URL
系统 SHALL 支持生成预签名下载 URL允许用户直接从对象存储下载文件。
#### Scenario: 生成下载 URL
- **WHEN** 调用 `GetDownloadURL(ctx, "exports/report.xlsx", 24*time.Hour)`
- **THEN** 方法 SHALL 返回有效的预签名 URL
- **THEN** URL SHALL 在指定时间24小时后过期
- **THEN** 使用该 URL 的 GET 请求 SHALL 能下载文件
---
### Requirement: 获取上传 URL API
系统 SHALL 提供 API 接口供前端获取预签名上传 URL。
接口定义:
```
POST /api/admin/storage/upload-url
Authorization: Bearer <token>
Content-Type: application/json
Request:
{
"file_name": "cards.csv",
"content_type": "text/csv",
"purpose": "iot_import"
}
Response:
{
"code": 0,
"message": "success",
"data": {
"upload_url": "http://obs-helf.cucloud.cn/cmp/imports/2025/01/24/abc123.csv?X-Amz-...",
"file_key": "imports/2025/01/24/abc123.csv",
"expires_in": 900
}
}
```
#### Scenario: 获取上传 URL 成功
- **WHEN** 已认证用户调用 POST /api/admin/storage/upload-url
- **AND** 请求包含有效的 file_name、content_type、purpose
- **THEN** 系统 SHALL 返回预签名上传 URL 和 file_key
- **THEN** file_key 格式 SHALL 为 `{purpose}/{year}/{month}/{day}/{uuid}.{ext}`
#### Scenario: 参数缺失
- **WHEN** 请求缺少必填参数
- **THEN** 系统 SHALL 返回 400 错误
#### Scenario: 未认证
- **WHEN** 请求未携带有效 Token
- **THEN** 系统 SHALL 返回 401 错误
---
### Requirement: 文件路径规范
系统 SHALL 按照规范生成文件路径。
路径格式:`{purpose}/{year}/{month}/{day}/{uuid}.{ext}`
支持的 purpose 值:
- `iot_import``imports/`
- `export``exports/`
- `attachment``attachments/`
#### Scenario: 生成导入文件路径
- **WHEN** purpose 为 "iot_import"file_name 为 "cards.csv"
- **THEN** 生成的 file_key SHALL 匹配 `imports/\d{4}/\d{2}/\d{2}/[a-f0-9-]+\.csv`
#### Scenario: 未知 purpose
- **WHEN** purpose 值不在支持列表中
- **THEN** 系统 SHALL 返回错误 "不支持的文件用途"
---
### Requirement: 配置结构
系统 SHALL 支持通过配置文件配置对象存储参数。
```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"
```
#### Scenario: 环境变量替换
- **WHEN** 配置值为 `${ENV_VAR}` 格式
- **THEN** 系统 SHALL 从环境变量读取实际值
#### Scenario: 临时目录不存在
- **WHEN** temp_dir 目录不存在
- **THEN** 系统 SHALL 自动创建该目录

View File

@@ -0,0 +1,219 @@
# 对象存储集成 - 任务清单
## 1. 基础设施
- [x] 1.1 在 `pkg/config/config.go` 添加 Storage 配置结构体
- [x] 1.2 在 `configs/config.yaml` 添加 storage 配置块(含环境变量占位符)
- [x] 1.3 创建 `pkg/storage/` 目录结构
## 2. Provider 实现
- [x] 2.1 创建 `pkg/storage/storage.go` - Provider 接口定义
- [x] 2.2 创建 `pkg/storage/types.go` - 公共类型PresignResult、Config
- [x] 2.3 创建 `pkg/storage/s3.go` - S3Provider 实现
- [x] 2.3.1 实现 NewS3Provider 构造函数
- [x] 2.3.2 实现 Upload 方法
- [x] 2.3.3 实现 Download 方法
- [x] 2.3.4 实现 DownloadToTemp 方法(含 cleanup 函数)
- [x] 2.3.5 实现 Delete 方法
- [x] 2.3.6 实现 Exists 方法
- [x] 2.3.7 实现 GetUploadURL 方法
- [x] 2.3.8 实现 GetDownloadURL 方法
- [x] 2.4 创建 `pkg/storage/service.go` - StorageService 封装
- [x] 2.4.1 实现 GenerateFileKey 方法purpose + 日期 + UUID
- [x] 2.4.2 实现 GetUploadURL 方法(生成 key + 获取预签名)
- [x] 2.4.3 实现 DownloadToTemp 方法(透传 + 日志)
## 3. Bootstrap 集成
- [x] 3.1 在 `internal/bootstrap/` 添加 storage 初始化逻辑
- [x] 3.2 在 `cmd/api/main.go` 集成 StorageService可选配置缺失时跳过
- [x] 3.3 在 `cmd/worker/main.go` 集成 StorageService
## 4. API 接口
- [x] 4.1 创建 `internal/model/dto/storage.go` - 请求/响应 DTO
- [x] 4.1.1 GetUploadURLRequestfile_name, content_type, purpose
- [x] 4.1.2 GetUploadURLResponseupload_url, file_key, expires_in
- [x] 4.2 创建 `internal/handler/admin/storage.go` - StorageHandler
- [x] 4.2.1 实现 GetUploadURL 方法
- [x] 4.3 注册路由 POST /api/admin/storage/upload-url
- [x] 4.3.1 在 RouteSpec.Description 添加前端使用流程说明Markdown 格式)
- [x] 4.4 更新 `cmd/api/docs.go``cmd/gendocs/main.go` 添加 StorageHandler
## 5. ICCID 导入改造
- [x] 5.1 修改 `internal/model/iot_card_import_task.go`
- [x] 5.1.1 添加 StorageBucket 字段
- [x] 5.1.2 添加 StorageKey 字段
- [x] 5.2 创建数据库迁移文件添加新字段
- [x] 5.3 修改 `internal/model/dto/iot_card_import.go`
- [x] 5.3.1 将 CreateImportTaskRequest 从 multipart 改为 JSON
- [x] 5.3.2 添加 FileKey 字段,移除 File 字段
- [x] 5.4 修改 `internal/handler/admin/iot_card_import.go`
- [x] 5.4.1 移除 c.FormFile() 逻辑
- [x] 5.4.2 改为接收 JSON body 解析 file_key
- [x] 5.4.3 保存 storage_bucket 和 storage_key 到任务记录
- [x] 5.6 更新导入接口的 RouteSpec.Description
- [x] 5.6.1 说明接口变更BREAKING: multipart → JSON
- [x] 5.6.2 说明完整导入流程(先获取上传 URL → 上传文件 → 调用导入接口)
- [x] 5.5 修改 `internal/task/iot_card_import.go`
- [x] 5.5.1 从任务记录获取 storage_key
- [x] 5.5.2 调用 StorageService.DownloadToTemp 下载文件
- [x] 5.5.3 处理完成后调用 cleanup() 删除临时文件
- [x] 5.5.4 保留原有 CSV 解析逻辑
## 6. 错误码
- [x] 6.1 在 `pkg/errors/codes.go` 添加存储相关错误码
- [x] 6.1.1 ErrStorageUploadFailed
- [x] 6.1.2 ErrStorageDownloadFailed
- [x] 6.1.3 ErrStorageFileNotFound
- [x] 6.1.4 ErrStorageInvalidPurpose
## 7. 测试
- [x] 7.1 创建 `scripts/test_storage.go` - 对象存储功能验证脚本
- [x] 7.2 联通云后台验证文件上传成功
- [x] 7.3 现有 Worker 测试通过
## 8. 文档
- [x] 8.1 创建 `docs/object-storage/使用指南.md` - 后端开发指南
- [x] 8.1.1 StorageService 使用示例
- [x] 8.1.2 配置说明
- [x] 8.1.3 错误处理
- [x] 8.2 创建 `docs/object-storage/前端接入指南.md` - 前端接入说明
- [x] 8.2.1 文件上传完整流程(时序图)
- [x] 8.2.2 获取预签名 URL 接口说明
- [x] 8.2.3 使用预签名 URL 上传文件(含代码示例)
- [x] 8.2.4 ICCID 导入接口变更说明BREAKING CHANGE
- [x] 8.2.5 错误处理和重试策略
- [x] 8.3 更新 README.md 添加对象存储功能说明
---
## 附录:前端接入指南内容大纲
### A. 文件上传流程(时序图)
```
前端 后端 API 对象存储
│ │ │
│ 1. POST /storage/upload-url │
│ {file_name, content_type, purpose} │
│ ─────────────────────────► │
│ │ │
│ 2. 返回 {upload_url, file_key, expires_in} │
│ ◄───────────────────────── │
│ │ │
│ 3. PUT upload_url (文件内容) │
│ ─────────────────────────────────────────────────► │
│ │ │
│ 4. 上传成功 (200 OK) │
│ ◄───────────────────────────────────────────────── │
│ │ │
│ 5. POST /iot-cards/import │
│ {carrier_id, batch_no, file_key} │
│ ─────────────────────────► │
│ │ │
│ 6. 返回任务创建成功 │
│ ◄───────────────────────── │
```
### B. 接口说明RouteSpec.Description 内容参考)
#### 获取上传 URL 接口
```markdown
## 文件上传流程
### 第一步:获取预签名 URL
调用本接口获取上传 URL 和 file_key。
### 第二步:直接上传到对象存储
使用返回的 `upload_url` 发起 PUT 请求上传文件:
\`\`\`javascript
const response = await fetch(upload_url, {
method: 'PUT',
headers: { 'Content-Type': content_type },
body: file
});
\`\`\`
### 第三步:使用 file_key 调用业务接口
上传成功后,使用 `file_key` 调用相关业务接口(如 ICCID 导入)。
### 注意事项
- 预签名 URL 有效期 15 分钟,请及时使用
- 上传失败时可重新获取 URL 重试
- file_key 在上传成功后永久有效
### purpose 可选值
| 值 | 说明 | 生成路径 |
|---|------|---------|
| iot_import | ICCID 导入 | imports/YYYY/MM/DD/uuid.csv |
| export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx |
| attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext |
```
#### ICCID 导入接口
```markdown
## ⚠️ 接口变更说明BREAKING CHANGE
本接口已从 `multipart/form-data` 改为 `application/json`
### 变更前
\`\`\`
POST /api/admin/iot-cards/import
Content-Type: multipart/form-data
carrier_id, batch_no, file (文件)
\`\`\`
### 变更后
\`\`\`
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"
}
\`\`\`
### 完整导入流程
1. 调用 `POST /api/admin/storage/upload-url` 获取上传 URL
2. 使用预签名 URL 上传 CSV 文件
3. 使用返回的 `file_key` 调用本接口
```
### C. 前端代码示例TypeScript
```typescript
// 完整的文件上传流程
async function uploadAndImport(file: File, carrierId: number, batchNo: string) {
// 1. 获取预签名 URL
const { data } = await api.post('/storage/upload-url', {
file_name: file.name,
content_type: file.type || 'text/csv',
purpose: 'iot_import'
});
const { upload_url, file_key } = data;
// 2. 上传文件到对象存储
await fetch(upload_url, {
method: 'PUT',
headers: { 'Content-Type': file.type || 'text/csv' },
body: file
});
// 3. 调用导入接口
return api.post('/iot-cards/import', {
carrier_id: carrierId,
batch_no: batchNo,
file_key: file_key
});
}
```