Files
digital-pilates/services/cos.ts
richarjiang 5d09cc05dc feat: 更新文章功能和相关依赖
- 新增文章详情页面,支持根据文章 ID 加载和展示文章内容
- 添加文章卡片组件,展示推荐文章的标题、封面和阅读量
- 更新文章服务,支持获取文章列表和根据 ID 获取文章详情
- 集成腾讯云 COS SDK,支持文件上传功能
- 优化打卡功能,支持按日期加载和展示打卡记录
- 更新相关依赖,确保项目兼容性和功能完整性
- 调整样式以适应新功能的展示和交互
2025-08-14 16:03:19 +08:00

188 lines
5.9 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.

import { COS_BUCKET, COS_REGION, buildPublicUrl } from '@/constants/Cos';
import { api } from '@/services/api';
import Cos from 'react-native-cos-sdk';
type ServerCosToken = {
tmpSecretId: string;
tmpSecretKey: string;
sessionToken: string;
startTime?: number;
expiredTime?: number;
bucket?: string;
region?: string;
prefix?: string;
cdnDomain?: string;
};
type CosCredential = {
credentials: {
tmpSecretId: string;
tmpSecretKey: string;
sessionToken: string;
};
startTime?: number;
expiredTime?: number;
bucket?: string;
region?: string;
prefix?: string;
cdnDomain?: string;
};
type UploadOptions = {
key: string;
// React Native COS SDK 推荐使用本地文件路径file:// 或 content://
srcUri?: string;
// 为兼容旧实现Web/Blob。在 RN SDK 下会忽略 body
body?: any;
contentType?: string;
onProgress?: (progress: { percent: number }) => void;
signal?: AbortSignal;
};
let rnTransferManager: any | null = null;
let rnInitialized = false;
async function fetchCredential(): Promise<CosCredential> {
// 后端返回 { code, message, data }api.get 会提取 data
const data = await api.get<ServerCosToken>('/api/users/cos/upload-token');
console.log('fetchCredential', data);
return {
credentials: {
tmpSecretId: data.tmpSecretId,
tmpSecretKey: data.tmpSecretKey,
sessionToken: data.sessionToken,
},
startTime: data.startTime,
expiredTime: data.expiredTime,
bucket: data.bucket,
region: data.region,
prefix: data.prefix,
cdnDomain: data.cdnDomain,
};
}
export async function uploadToCos(options: UploadOptions): Promise<{ key: string; etag?: string; headers?: Record<string, string>; publicUrl?: string }> {
const { key, srcUri, contentType, onProgress, signal } = options;
const cred = await fetchCredential();
const bucket = COS_BUCKET || cred.bucket;
const region = COS_REGION || cred.region;
if (!bucket || !region) {
throw new Error('未配置 COS_BUCKET / COS_REGION且服务端未返回 bucket/region');
}
// 确保对象键以服务端授权的前缀开头(去除通配符,折叠斜杠,避免把 * 拼进 Key
const finalKey = ((): string => {
const rawPrefix = String(cred.prefix || '');
// 1) 去掉 * 与多余斜杠,再去掉首尾斜杠
const safePrefix = rawPrefix
.replace(/\*/g, '')
.replace(/\/{2,}/g, '/')
.replace(/^\/+|\/+$/g, '');
const normalizedKey = key.replace(/^\//, '');
if (!safePrefix) return normalizedKey;
if (normalizedKey.startsWith(safePrefix + '/')) return normalizedKey;
return `${safePrefix}/${normalizedKey}`.replace(/\/{2,}/g, '/');
})();
// 初始化 react-native-cos-sdk一次
if (!rnInitialized) {
await Cos.initWithSessionCredentialCallback(async () => {
// SDK 会在需要时调用该回调,我们返回当前的临时密钥
return {
tmpSecretId: cred.credentials.tmpSecretId,
tmpSecretKey: cred.credentials.tmpSecretKey,
sessionToken: cred.credentials.sessionToken,
startTime: cred.startTime,
expiredTime: cred.expiredTime,
} as any;
});
const serviceConfig = { region, isDebuggable: true, isHttps: true } as any;
await Cos.registerDefaultService(serviceConfig);
const transferConfig = {
forceSimpleUpload: false,
enableVerification: true,
divisionForUpload: 2 * 1024 * 1024,
sliceSizeForUpload: 1 * 1024 * 1024,
} as any;
rnTransferManager = await Cos.registerDefaultTransferManger(serviceConfig, transferConfig);
rnInitialized = true;
}
if (!srcUri || typeof srcUri !== 'string') {
throw new Error('请提供本地文件路径 srcUri形如 file:/// 或 content://');
}
const controller = new AbortController();
if (signal) {
if (signal.aborted) controller.abort();
signal.addEventListener('abort', () => controller.abort(), { once: true });
}
return await new Promise((resolve, reject) => {
let cancelled = false;
let taskRef: any = null;
(async () => {
try {
taskRef = await rnTransferManager.upload(
bucket,
finalKey,
srcUri,
{
resultListener: {
successCallBack: (header: any) => {
if (cancelled) return;
const publicUrl = buildPublicUrl(finalKey);
const etag = header?.ETag || header?.headers?.ETag || header?.headers?.etag;
resolve({ key: finalKey, etag, headers: header, publicUrl });
},
failCallBack: (clientError: any, serviceError: any) => {
if (cancelled) return;
console.log('uploadToCos', { clientError, serviceError });
const err = clientError || serviceError || new Error('COS 上传失败');
reject(err);
},
},
progressCallback: (complete: number, target: number) => {
if (onProgress) {
const percent = target > 0 ? complete / target : 0;
onProgress({ percent });
}
},
contentType,
}
);
} catch (e) {
if (!cancelled) reject(e);
}
})();
controller.signal.addEventListener('abort', () => {
cancelled = true;
try { taskRef?.cancel?.(); } catch { }
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
export async function uploadWithRetry(options: UploadOptions & { maxRetries?: number; backoffMs?: number }): Promise<{ key: string; etag?: string }> {
const { maxRetries = 2, backoffMs = 800, ...rest } = options;
let attempt = 0;
// 简单指数退避
while (true) {
try {
return await uploadToCos(rest);
} catch (e: any) {
if (rest.signal?.aborted) throw e;
if (attempt >= maxRetries) throw e;
const wait = backoffMs * Math.pow(2, attempt);
await new Promise(r => setTimeout(r, wait));
attempt++;
}
}
}