package routes import ( "github.com/gofiber/fiber/v2" "github.com/break/junhong_cmp_fiber/internal/handler/admin" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/openapi" ) func registerStorageRoutes(router fiber.Router, handler *admin.StorageHandler, doc *openapi.Generator, basePath string) { storage := router.Group("/storage") groupPath := basePath + "/storage" Register(storage, doc, groupPath, "POST", "/batch-download-urls", handler.GetBatchDownloadURLs, RouteSpec{ Summary: "批量获取文件下载预签名 URL", Description: `## 文件展示流程 本接口用于批量获取对象存储文件的预签名下载 URL。由于存储桶为私有访问,文件不能直接通过路径访问,必须通过预签名 URL 才能展示。 ### 为什么需要这个接口 对象存储使用私有 Bucket,文件路径(file_key)本身无法直接访问。前端持有的 file_key 只是文件标识,需要通过本接口换取带签名的临时访问 URL,才能在 ` + "`" + `` + "`" + ` 或下载链接中使用。 ### 完整使用流程 1. **业务接口返回 file_key**(存储在数据库中的文件路径标识) 2. **调用本接口** 传入所有需要展示的 file_key 列表 3. **使用返回的 URL** 直接赋值给 ` + "`" + `` + "`" + ` 或下载链接,图片/文件内容直接从对象存储加载,不经过后端 ### 列表页批量展示示例 ` + "```" + `javascript // 假设列表数据中每条记录有 avatar_key 字段 const listData = await api.get('/customers'); // 收集所有 file_key(过滤掉空值) const fileKeys = listData.map(item => item.avatar_key).filter(Boolean); // 一次性换取所有下载 URL const { data } = await api.post('/storage/batch-download-urls', { file_keys: fileKeys }); // 渲染时通过 map 取对应 URL listData.forEach(item => { item.avatarUrl = data.urls[item.avatar_key] ?? ''; }); ` + "```" + ` ### 单个文件展示示例 ` + "```" + `javascript // 详情页只有一个文件时,也走批量接口(保持一致) const { data } = await api.post('/storage/batch-download-urls', { file_keys: [record.license_key] }); const licenseUrl = data.urls[record.license_key]; ` + "```" + ` ### 前端缓存建议 预签名 URL 有效期 **24 小时**,建议在内存中缓存,避免同一页面重复请求: ` + "```" + `javascript const urlCache = new Map(); // key: file_key, value: { url, expireAt } async function getFileUrl(fileKey) { const cached = urlCache.get(fileKey); if (cached && cached.expireAt > Date.now()) { return cached.url; } const { data } = await api.post('/storage/batch-download-urls', { file_keys: [fileKey] }); const url = data.urls[fileKey]; urlCache.set(fileKey, { url, expireAt: Date.now() + data.expires_in * 1000 * 0.9 }); return url; } ` + "```" + ` ### 注意事项 - 单次请求最多 **50 个** file_key - 预签名 URL 有效期 **24 小时**,过期后需重新获取 - file_key 不存在时对应的 URL 生成会失败,整个请求返回错误 - 图片/文件内容直接从对象存储加载到浏览器,**不经过后端**,不消耗服务器带宽`, Tags: []string{"对象存储"}, Input: new(dto.GetBatchDownloadURLsRequest), Output: new(dto.GetBatchDownloadURLsResponse), Auth: true, }) Register(storage, doc, groupPath, "POST", "/upload-url", handler.GetUploadURL, RouteSpec{ Summary: "获取文件上传预签名 URL", Description: `## 文件上传流程 本接口用于获取对象存储的预签名上传 URL,实现前端直传文件到对象存储。 ### 完整流程 1. **调用本接口** 获取预签名 URL 和 file_key 2. **使用预签名 URL 上传文件** 发起 PUT 请求直接上传到对象存储 3. **调用业务接口** 使用 file_key 调用相关业务接口(如 ICCID 导入) ### 前端上传示例 ` + "```" + `javascript // 1. 获取预签名 URL const { data } = await api.post('/storage/upload-url', { file_name: 'cards.xlsx', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', purpose: 'iot_import' }); // 2. 上传文件到对象存储 await fetch(data.upload_url, { method: 'PUT', headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, body: file }); // 3. 调用业务接口 await api.post('/iot-cards/import', { carrier_id: 1, batch_no: 'BATCH-2025-01', file_key: data.file_key }); ` + "```" + ` ### purpose 可选值 | 值 | 说明 | 生成路径格式 | |---|------|-------------| | iot_import | ICCID/设备导入 (Excel) | imports/YYYY/MM/DD/uuid.xlsx | | export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx | | attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext | ### 注意事项 - 预签名 URL 有效期 **15 分钟**,请及时使用 - 上传时 Content-Type 需与请求时一致 - file_key 在上传成功后永久有效,用于后续业务接口调用 - 上传失败时可重新调用本接口获取新的 URL`, Tags: []string{"对象存储"}, Input: new(dto.GetUploadURLRequest), Output: new(dto.GetUploadURLResponse), Auth: true, }) }