Files
one-pipe-system/src/api/modules/storage.ts
luo 06684e1fc0
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s
fix: 轮询按运营商账户限流
2026-10-26 14:31:15 +08:00

191 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 对象存储相关 API
*/
import { BaseService } from '../BaseService'
import type { BaseResponse } from '@/types/api'
const inferFileNameFromKey = (fileKey: string) => {
return fileKey.split('/').pop() || 'download'
}
const triggerBrowserDownload = (downloadUrl: string, fileName: string) => {
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName
link.target = '_blank'
link.rel = 'noopener noreferrer'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/**
* 文件用途枚举
*/
export type FilePurpose =
| 'polling_status_import'
| 'iot_import'
| 'batch_purchase'
| 'device_batch_allocation'
| 'shop_import'
| 'phone_unbind_import'
| 'export'
| 'attachment'
/**
* 获取上传 URL 请求参数
*/
export interface GetUploadUrlRequest {
/** 文件名(如:cards.csv) */
file_name: string
/** 文件 MIME 类型(如:text/csv),留空则自动推断 */
content_type?: string
/** 文件用途 (iot_import:ICCID导入, export:数据导出, attachment:附件) */
purpose: FilePurpose
}
/**
* 获取上传 URL 响应
*/
export interface GetUploadUrlResponse {
/** 预签名上传 URL,使用 PUT 方法上传文件 */
upload_url: string
/** 文件路径标识,上传成功后用于调用业务接口 */
file_key: string
/** URL 有效期(秒) */
expires_in: number
}
/**
* 批量获取文件下载预签名 URL 请求
*/
export interface BatchDownloadUrlsRequest {
/** 文件路径标识列表,最多 50 个 */
file_keys: string[]
}
/**
* 批量获取文件下载预签名 URL 响应
*/
export interface BatchDownloadUrlsResponse {
/** 文件下载地址映射,key 为 file_key */
urls: Record<string, string>
/** URL 有效期(秒) */
expires_in: number
}
export class StorageService extends BaseService {
/**
* 获取文件上传预签名 URL
*
* ## 完整上传流程
* 1. 调用本接口获取预签名 URL 和 file_key
* 2. 使用预签名 URL 上传文件(发起 PUT 请求直接上传到对象存储)
* 3. 使用 file_key 调用相关业务接口
*
* @param data 请求参数
*/
static getUploadUrl(data: GetUploadUrlRequest): Promise<BaseResponse<GetUploadUrlResponse>> {
return this.post<BaseResponse<GetUploadUrlResponse>>('/api/admin/storage/upload-url', data)
}
/**
* 使用预签名 URL 上传文件到对象存储
*
* 注意事项:
* - 预签名 URL 有效期 15 分钟,请及时使用
* - 上传时 Content-Type 需与请求时一致
* - file_key 在上传成功后永久有效
* - 开发环境通过代理上传,生产环境直接上传
*
* @param uploadUrl 预签名 URL(由对象存储生成)
* @param file 文件
* @param contentType 文件类型(需与 getUploadUrl 请求时保持一致)
*/
static async uploadFile(uploadUrl: string, file: File, contentType?: string): Promise<void> {
try {
// 开发环境使用代理解决 CORS 问题
let finalUrl = uploadUrl
if (import.meta.env.DEV) {
// 将对象存储域名替换为代理路径
finalUrl = uploadUrl.replace(/^https?:\/\/obs-helf\.cucloud\.cn/, '/obs-proxy')
}
const headers: Record<string, string> = {}
// 只有在明确指定 contentType 时才设置,否则让浏览器自动处理
if (contentType) {
headers['Content-Type'] = contentType
}
const response = await fetch(finalUrl, {
method: 'PUT',
body: file,
headers
})
if (!response.ok) {
const errorText = await response.text().catch(() => response.statusText)
throw new Error(`上传文件失败 (${response.status}): ${errorText}`)
}
} catch (error: any) {
// 增强错误信息
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
throw new Error(
'CORS 错误: 无法上传文件到对象存储。' +
'这通常是因为对象存储服务器未正确配置 CORS 策略。' +
'请联系后端开发人员检查对象存储的 CORS 配置。'
)
}
throw error
}
}
/**
* 批量获取文件下载预签名 URL
* 用于获取私有文件的临时下载地址
* @param data 请求参数
*/
static batchDownloadUrls(
data: BatchDownloadUrlsRequest
): Promise<BaseResponse<BatchDownloadUrlsResponse>> {
return this.post<BaseResponse<BatchDownloadUrlsResponse>>(
'/api/admin/storage/batch-download-urls',
data
)
}
/**
* 获取单文件下载 URL
* @param fileKey 文件路径标识
*/
static async getDownloadUrl(fileKey: string): Promise<string> {
const normalizedFileKey = fileKey.trim()
const res = await this.batchDownloadUrls({
file_keys: [normalizedFileKey]
})
if (res.code !== 0 || !res.data?.urls?.[normalizedFileKey]) {
throw new Error(res.msg || '获取下载地址失败')
}
return res.data.urls[normalizedFileKey]
}
/**
* 按文件标识直接触发浏览器下载
* @param fileKey 文件路径标识
* @param fileName 可选文件名
*/
static async downloadFileByKey(fileKey: string, fileName?: string): Promise<void> {
const normalizedFileKey = fileKey.trim()
if (!normalizedFileKey) {
throw new Error('文件标识不能为空')
}
const downloadUrl = await this.getDownloadUrl(normalizedFileKey)
triggerBrowserDownload(downloadUrl, fileName || inferFileNameFromKey(normalizedFileKey))
}
}