# 对象存储集成 - 任务清单 ## 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 GetUploadURLRequest(file_name, content_type, purpose) - [x] 4.1.2 GetUploadURLResponse(upload_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 }); } ```