252 lines
7.9 KiB
JavaScript
252 lines
7.9 KiB
JavaScript
import { getOssPresignedUrlApi } from '@/utils/api.js'
|
||
|
||
/**
|
||
* 预签名 URL 内存缓存
|
||
* key = 原始 cover_url(相对路径 / 完整带 Expires 的 URL)
|
||
* value = 已签名的完整 URL
|
||
*
|
||
* 命中规则:
|
||
* - 命中后再检查一次 isExpiredPresignedUrl,过期则丢弃重签
|
||
* - 同一进程内(页面 reload 会清);上限 500 条,超过随机清一半
|
||
*/
|
||
const coverUrlCache = new Map()
|
||
const COVER_CACHE_LIMIT = 500
|
||
|
||
function setCoverCache(rawUrl, signedUrl) {
|
||
if (!rawUrl || !signedUrl) return
|
||
if (coverUrlCache.size >= COVER_CACHE_LIMIT) {
|
||
// 简单容量控制:删一半旧的,避免无限增长
|
||
const keys = Array.from(coverUrlCache.keys()).slice(0, COVER_CACHE_LIMIT / 2)
|
||
keys.forEach(k => coverUrlCache.delete(k))
|
||
}
|
||
coverUrlCache.set(rawUrl, signedUrl)
|
||
}
|
||
|
||
function getCoverCache(rawUrl) {
|
||
const v = coverUrlCache.get(rawUrl)
|
||
if (!v) return null
|
||
if (isExpiredPresignedUrl(v)) {
|
||
coverUrlCache.delete(rawUrl)
|
||
return null
|
||
}
|
||
return v
|
||
}
|
||
|
||
/**
|
||
* 从URL提取文件名
|
||
* @param {String} url - URL字符串
|
||
* @returns {String|null} 提取的文件名,失败返回null
|
||
*/
|
||
export function extractFileNameFromUrl(url) {
|
||
if (!url || url.startsWith('/static/')) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
let urlPath = url;
|
||
|
||
// 如果是完整URL,提取路径部分
|
||
if (url.includes('://')) {
|
||
// 找到协议后的第一个 / 开始的路径
|
||
const protocolEndIndex = url.indexOf('://');
|
||
const pathStartIndex = url.indexOf('/', protocolEndIndex + 3);
|
||
if (pathStartIndex !== -1) {
|
||
urlPath = url.substring(pathStartIndex);
|
||
}
|
||
}
|
||
|
||
// 提取最后一个 / 之后的内容作为文件名
|
||
const fileName = urlPath.substring(urlPath.lastIndexOf('/') + 1);
|
||
return fileName || null;
|
||
} catch (error) {
|
||
console.error('提取文件名失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从cover_url提取文件名(兼容旧函数名)
|
||
* @param {String} coverUrl - 后端返回的cover_url
|
||
* @returns {String|null} 提取的文件名,失败返回null
|
||
*/
|
||
export function extractFileNameFromCoverUrl(coverUrl) {
|
||
return extractFileNameFromUrl(coverUrl);
|
||
}
|
||
|
||
/**
|
||
* 从OSS完整URL中提取对象路径(保留bucket之后的完整路径)
|
||
* 例如:https://bucket.oss-cn-shanghai.aliyuncs.com/asset/13/87/file.jpg -> asset/13/87/file.jpg
|
||
* @param {String} url - OSS完整URL
|
||
* @returns {String|null} 对象路径,失败返回null
|
||
*/
|
||
export function extractOssObjectPath(url) {
|
||
if (!url || url.startsWith('/static/')) return null;
|
||
try {
|
||
if (url.includes('aliyuncs.com/')) {
|
||
return url.substring(url.indexOf('aliyuncs.com/') + 'aliyuncs.com/'.length);
|
||
}
|
||
// 兜底:返回域名后的路径
|
||
const protocolEnd = url.indexOf('://');
|
||
const pathStart = url.indexOf('/', protocolEnd + 3);
|
||
if (pathStart !== -1) {
|
||
return url.substring(pathStart + 1);
|
||
}
|
||
return null;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 判断 URL 是否为带 Expires 的 OSS 预签名 URL,且已过期
|
||
* @param {String} url
|
||
* @returns {Boolean}
|
||
*/
|
||
function isExpiredPresignedUrl(url) {
|
||
if (!url || typeof url !== 'string') return false
|
||
const m = url.match(/[?&]Expires=(\d+)/)
|
||
if (!m) return false
|
||
// OSS Expires 单位是秒,Date.now() 单位是毫秒
|
||
return Number(m[1]) * 1000 < Date.now()
|
||
}
|
||
|
||
/**
|
||
* 从完整 OSS URL 提取对象路径(去掉 ?Expires=...&OSSAccessKeyId=...&Signature=... 部分)
|
||
* 例如:https://bucket.oss-cn-shanghai.aliyuncs.com/laser-card/x.png?Expires=... → laser-card/x.png
|
||
* @param {String} url
|
||
* @returns {String}
|
||
*/
|
||
function extractOssObjectPathFromUrl(url) {
|
||
try {
|
||
// 去掉 query string
|
||
const pathOnly = url.split('?')[0]
|
||
// 提取 aliyuncs.com/ 之后的路径
|
||
const marker = 'aliyuncs.com/'
|
||
const idx = pathOnly.indexOf(marker)
|
||
if (idx !== -1) {
|
||
return pathOnly.substring(idx + marker.length)
|
||
}
|
||
// 兜底:去掉协议 + 域名
|
||
const protoEnd = pathOnly.indexOf('://')
|
||
const pathStart = pathOnly.indexOf('/', protoEnd + 3)
|
||
if (pathStart !== -1) {
|
||
return pathOnly.substring(pathStart + 1)
|
||
}
|
||
return pathOnly
|
||
} catch (e) {
|
||
return url
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 同步获取「立即可用的 cover URL」,绝不发起网络请求。
|
||
* 用于:先用这个 URL 立即渲染列表,再异步调 getAssetCoverRealUrl 拿到精确的预签名 URL 并替换。
|
||
*
|
||
* 优先级:
|
||
* 1) /static/... 或空 → 兜底占位图
|
||
* 2) 命中缓存 → 缓存内的预签名 URL
|
||
* 3) 完整 URL 未过期 → 原样返回
|
||
* 4) 其它(相对路径 / 过期 URL) → 占位图(等异步预签名补上)
|
||
*
|
||
* @param {String} coverUrl
|
||
* @param {String} [placeholder='/static/nft/collection.png']
|
||
* @returns {String}
|
||
*/
|
||
export function getInstantAssetCoverUrl(coverUrl, placeholder = '/static/nft/collection.png') {
|
||
if (!coverUrl || coverUrl.startsWith('/static/')) {
|
||
return coverUrl || placeholder
|
||
}
|
||
const cached = getCoverCache(coverUrl)
|
||
if (cached) return cached
|
||
if (coverUrl.includes('://') && !isExpiredPresignedUrl(coverUrl)) {
|
||
return coverUrl
|
||
}
|
||
return placeholder
|
||
}
|
||
|
||
/**
|
||
* 获取藏品封面的真实URL - 使用 OSS 预签名 URL
|
||
* 处理 3 种 coverUrl 形态:
|
||
* 1) /static/... 本地静态资源 → 原样返回
|
||
* 2) 相对路径 asset/1/87/xxx.jpg → 调 getOssPresignedUrlApi 重签
|
||
* 3) 完整 OSS 预签名 URL(可能已过期) → 检测 Expires,过期则提取对象路径重签
|
||
* @param {String} coverUrl - 后端返回的cover_url
|
||
* @returns {Promise<String>} 真实可访问的URL
|
||
*/
|
||
export async function getAssetCoverRealUrl(coverUrl) {
|
||
// 默认图片路径
|
||
const DEFAULT_IMAGE = '/static/nft/collection.png';
|
||
|
||
// 1. 本地静态资源或空 → 直接返回
|
||
if (!coverUrl || coverUrl.startsWith('/static/')) {
|
||
return coverUrl || DEFAULT_IMAGE;
|
||
}
|
||
|
||
// 命中缓存:直接返回(已在 getCoverCache 内做过期检查)
|
||
const cached = getCoverCache(coverUrl)
|
||
if (cached) return cached
|
||
|
||
// 2. 完整 URL(带 ://)
|
||
if (coverUrl.includes('://')) {
|
||
// 检查是否带 Expires 且已过期 → 提取对象路径重签
|
||
if (isExpiredPresignedUrl(coverUrl)) {
|
||
const objectPath = extractOssObjectPathFromUrl(coverUrl)
|
||
try {
|
||
const res = await getOssPresignedUrlApi(objectPath, 3600, 'asset')
|
||
if (res && res.data && res.data.url) {
|
||
setCoverCache(coverUrl, res.data.url)
|
||
return res.data.url
|
||
}
|
||
} catch (e) {
|
||
console.error('[assetImageHelper] re-sign expired url failed:', e)
|
||
}
|
||
return objectPath // 兜底:返回相对路径
|
||
}
|
||
// 未过期:原样返回 + 顺手缓存
|
||
setCoverCache(coverUrl, coverUrl)
|
||
return coverUrl
|
||
}
|
||
|
||
// 3. 相对路径:调预签名接口
|
||
try {
|
||
const res = await getOssPresignedUrlApi(coverUrl, 3600, 'asset');
|
||
if (res && res.data && res.data.url) {
|
||
setCoverCache(coverUrl, res.data.url)
|
||
return res.data.url;
|
||
}
|
||
return coverUrl;
|
||
} catch (e) {
|
||
console.error('[assetImageHelper] get presigned url failed:', e);
|
||
return coverUrl;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取好友头像的真实URL - 使用 OSS 预签名 URL
|
||
* @param {String} avatarUrl - 后端返回的avatar_url
|
||
* @returns {Promise<String>} 真实可访问的URL,失败返回空字符串
|
||
*/
|
||
export async function getFriendAvatarRealUrl(avatarUrl) {
|
||
// 如果是本地静态资源或为空,直接返回
|
||
if (!avatarUrl || avatarUrl.startsWith('/static/')) {
|
||
return avatarUrl || '';
|
||
}
|
||
|
||
// 如果是完整的绝对URL(包含 :://),直接返回
|
||
if (avatarUrl.includes('://')) {
|
||
return avatarUrl;
|
||
}
|
||
|
||
// 相对路径:调用预签名接口获取可访问的URL
|
||
try {
|
||
const res = await getOssPresignedUrlApi(avatarUrl, 3600, 'avatar');
|
||
if (res && res.data && res.data.url) {
|
||
return res.data.url;
|
||
}
|
||
// 预签名失败,返回原始路径
|
||
return avatarUrl;
|
||
} catch (e) {
|
||
console.error('[assetImageHelper] get avatar presigned url failed:', e);
|
||
return avatarUrl;
|
||
}
|
||
} |