topfans/frontend/utils/assetImageHelper.js
2026-06-11 13:56:34 +08:00

252 lines
7.9 KiB
JavaScript
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 { 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;
}
}