// 媒体文件管理器 - 微信小程序专用 // 处理图片、视频、音频、文件的上传、下载、预览、缓存等 const apiClient = require('../../../utils/api-client.js'); /** * 媒体文件管理器 * 功能: * 1. 文件选择和上传 * 2. 媒体文件预览 * 3. 文件下载和缓存 * 4. 文件压缩和优化 * 5. 云存储管理 * 6. 文件类型检测 */ class MediaManager { constructor() { this.isInitialized = false; // 媒体配置 this.mediaConfig = { // 图片配置 image: { maxSize: 10 * 1024 * 1024, // 10MB maxCount: 9, // 最多选择9张 quality: 80, // 压缩质量 formats: ['jpg', 'jpeg', 'png', 'gif', 'webp'], compressWidth: 1080 // 压缩宽度 }, // 视频配置 video: { maxSize: 100 * 1024 * 1024, // 100MB maxDuration: 300, // 最长5分钟 formats: ['mp4', 'mov', 'avi'], compressQuality: 'medium' }, // 音频配置 audio: { maxSize: 20 * 1024 * 1024, // 20MB maxDuration: 600, // 最长10分钟 formats: ['mp3', 'wav', 'aac', 'm4a'], sampleRate: 16000 }, // 文件配置 file: { maxSize: 50 * 1024 * 1024, // 50MB allowedTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar'], maxCount: 5 }, // 缓存配置 cache: { maxSize: 200 * 1024 * 1024, // 200MB expireTime: 7 * 24 * 60 * 60 * 1000, // 7天 cleanupInterval: 24 * 60 * 60 * 1000 // 24小时清理一次 } }; // 文件缓存 this.fileCache = new Map(); // 上传队列 this.uploadQueue = []; // 下载队列 this.downloadQueue = []; // 当前上传任务 this.currentUploads = new Map(); // 缓存统计 this.cacheStats = { totalSize: 0, fileCount: 0, lastCleanup: 0 }; this.init(); } // 初始化媒体管理器 async init() { if (this.isInitialized) return; try { // 加载文件缓存信息 await this.loadCacheInfo(); // 检查存储权限 await this.checkStoragePermission(); // 启动缓存清理 this.startCacheCleanup(); this.isInitialized = true; } catch (error) { console.error('❌ 媒体文件管理器初始化失败:', error); } } // 📷 ===== 图片处理 ===== // 选择图片 async chooseImages(options = {}) { try { const { count = this.mediaConfig.image.maxCount, sizeType = ['compressed', 'original'], sourceType = ['album', 'camera'] } = options; const result = await new Promise((resolve, reject) => { wx.chooseImage({ count: count, sizeType: sizeType, sourceType: sourceType, success: resolve, fail: reject }); }); // 验证和处理图片 const processedImages = await this.processImages(result.tempFilePaths); return { success: true, images: processedImages }; } catch (error) { console.error('❌ 选择图片失败:', error); return { success: false, error: error.errMsg || '选择图片失败' }; } } // 处理图片 async processImages(tempFilePaths) { const processedImages = []; for (const tempPath of tempFilePaths) { try { // 获取图片信息 const imageInfo = await this.getImageInfo(tempPath); // 检查文件大小 if (imageInfo.size > this.mediaConfig.image.maxSize) { console.warn('⚠️ 图片过大,需要压缩:', imageInfo.size); // 压缩图片 const compressedPath = await this.compressImage(tempPath); imageInfo.tempFilePath = compressedPath; } // 生成缩略图 const thumbnailPath = await this.generateThumbnail(imageInfo.tempFilePath); processedImages.push({ ...imageInfo, thumbnailPath: thumbnailPath, type: 'image', status: 'ready' }); } catch (error) { console.error('❌ 处理图片失败:', error); } } return processedImages; } // 获取图片信息 async getImageInfo(src) { return new Promise((resolve, reject) => { wx.getImageInfo({ src: src, success: (res) => { // 获取文件大小 wx.getFileInfo({ filePath: src, success: (fileInfo) => { resolve({ ...res, size: fileInfo.size, tempFilePath: src }); }, fail: () => { resolve({ ...res, size: 0, tempFilePath: src }); } }); }, fail: reject }); }); } // 压缩图片 // 图片压缩处理函数 compressImage(tempFilePath) { return new Promise((resolve, reject) => { // 先获取图片信息,用于计算压缩比例 wx.getImageInfo({ src: tempFilePath, success: (info) => { // 根据图片尺寸计算合适的压缩比例 let quality = 0.6; // 初始质量 let width = info.width; let height = info.height; // 对于过大的图片先缩小尺寸 if (width > 1000 || height > 1000) { const scale = 1000 / Math.max(width, height); width = Math.floor(width * scale); height = Math.floor(height * scale); quality = 0.5; // 大图片适当降低质量 } else if (width > 500 || height > 500) { quality = 0.55; // 中等图片适度压缩 } // 执行压缩 wx.compressImage({ src: tempFilePath, quality: quality, // 质量,0-100,这里用0.6表示60%质量 width: width, // 压缩后的宽度 height: height, // 压缩后的高度 success: (res) => { resolve(res.tempFilePath); }, fail: (err) => { reject(err); } }); }, fail: (err) => { reject(err); } }); }); } // 生成缩略图 async generateThumbnail(src) { try { // 创建canvas生成缩略图 const canvas = wx.createOffscreenCanvas({ type: '2d' }); const ctx = canvas.getContext('2d'); // 设置缩略图尺寸 const thumbnailSize = 200; canvas.width = thumbnailSize; canvas.height = thumbnailSize; // 加载图片 const image = canvas.createImage(); return new Promise((resolve) => { image.onload = () => { // 计算绘制尺寸 const { drawWidth, drawHeight, drawX, drawY } = this.calculateDrawSize( image.width, image.height, thumbnailSize, thumbnailSize ); // 绘制缩略图 ctx.drawImage(image, drawX, drawY, drawWidth, drawHeight); // 导出为临时文件 wx.canvasToTempFilePath({ canvas: canvas, success: (res) => { resolve(res.tempFilePath); }, fail: () => { resolve(src); // 生成失败返回原图 } }); }; image.onerror = () => { resolve(src); // 加载失败返回原图 }; image.src = src; }); } catch (error) { console.error('❌ 生成缩略图失败:', error); return src; } } // 计算绘制尺寸 calculateDrawSize(imageWidth, imageHeight, canvasWidth, canvasHeight) { const imageRatio = imageWidth / imageHeight; const canvasRatio = canvasWidth / canvasHeight; let drawWidth, drawHeight, drawX, drawY; if (imageRatio > canvasRatio) { // 图片更宽,以高度为准 drawHeight = canvasHeight; drawWidth = drawHeight * imageRatio; drawX = (canvasWidth - drawWidth) / 2; drawY = 0; } else { // 图片更高,以宽度为准 drawWidth = canvasWidth; drawHeight = drawWidth / imageRatio; drawX = 0; drawY = (canvasHeight - drawHeight) / 2; } return { drawWidth, drawHeight, drawX, drawY }; } // 🎬 ===== 视频处理 ===== // 选择视频 async chooseVideo(options = {}) { try { const { sourceType = ['album', 'camera'], maxDuration = this.mediaConfig.video.maxDuration, camera = 'back' } = options; const result = await new Promise((resolve, reject) => { wx.chooseVideo({ sourceType: sourceType, maxDuration: maxDuration, camera: camera, success: resolve, fail: reject }); }); // 验证和处理视频 const processedVideo = await this.processVideo(result); return { success: true, video: processedVideo }; } catch (error) { console.error('❌ 选择视频失败:', error); return { success: false, error: error.errMsg || '选择视频失败' }; } } // 处理视频 async processVideo(videoResult) { try { // 检查文件大小 if (videoResult.size > this.mediaConfig.video.maxSize) { throw new Error('视频文件过大'); } // 检查时长 if (videoResult.duration > this.mediaConfig.video.maxDuration) { throw new Error('视频时长超出限制'); } // 生成视频缩略图 const thumbnailPath = await this.generateVideoThumbnail(videoResult.tempFilePath); return { tempFilePath: videoResult.tempFilePath, duration: videoResult.duration, size: videoResult.size, width: videoResult.width, height: videoResult.height, thumbnailPath: thumbnailPath, type: 'video', status: 'ready' }; } catch (error) { console.error('❌ 处理视频失败:', error); throw error; } } // 生成视频缩略图 async generateVideoThumbnail(videoPath) { try { // 微信小程序暂不支持视频帧提取,使用默认图标 return null; } catch (error) { console.error('❌ 生成视频缩略图失败:', error); return null; } } // 📄 ===== 文件处理 ===== // 选择文件 async chooseFile(options = {}) { try { const { count = this.mediaConfig.file.maxCount, type = 'all' } = options; const result = await new Promise((resolve, reject) => { wx.chooseMessageFile({ count: count, type: type, success: resolve, fail: reject }); }); // 验证和处理文件 const processedFiles = await this.processFiles(result.tempFiles); return { success: true, files: processedFiles }; } catch (error) { console.error('❌ 选择文件失败:', error); return { success: false, error: error.errMsg || '选择文件失败' }; } } // 处理文件 async processFiles(tempFiles) { const processedFiles = []; for (const file of tempFiles) { try { // 检查文件大小 if (file.size > this.mediaConfig.file.maxSize) { console.warn('⚠️ 文件过大:', file.name, file.size); continue; } // 检查文件类型 const fileExtension = this.getFileExtension(file.name); if (!this.isAllowedFileType(fileExtension)) { console.warn('⚠️ 不支持的文件类型:', fileExtension); continue; } processedFiles.push({ tempFilePath: file.path, name: file.name, size: file.size, type: 'file', extension: fileExtension, status: 'ready' }); } catch (error) { console.error('❌ 处理文件失败:', error); } } return processedFiles; } // 获取文件扩展名 getFileExtension(fileName) { const lastDotIndex = fileName.lastIndexOf('.'); if (lastDotIndex === -1) return ''; return fileName.substring(lastDotIndex + 1).toLowerCase(); } // 检查文件类型是否允许 isAllowedFileType(extension) { return this.mediaConfig.file.allowedTypes.includes(extension); } // 📤 ===== 文件上传 ===== // 上传文件 async uploadFile(file, options = {}) { try { const { onProgress, onSuccess, onError } = options; // 生成上传ID const uploadId = this.generateUploadId(); // 添加到上传队列 const uploadTask = { id: uploadId, file: file, status: 'uploading', progress: 0, onProgress: onProgress, onSuccess: onSuccess, onError: onError }; this.currentUploads.set(uploadId, uploadTask); // 执行上传 const result = await this.performUpload(uploadTask); // 移除上传任务 this.currentUploads.delete(uploadId); return result; } catch (error) { console.error('❌ 上传文件失败:', error); return { success: false, error: error.message }; } } // 执行上传 async performUpload(uploadTask) { return new Promise((resolve, reject) => { const uploadTask_wx = wx.uploadFile({ url: `${apiClient.baseURL}/api/v1/files/upload`, filePath: uploadTask.file.tempFilePath, name: 'file', header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` }, formData: { type: uploadTask.file.type, name: uploadTask.file.name || 'unknown' }, success: (res) => { try { const data = JSON.parse(res.data); if (data.success) { const result = { success: true, data: { url: data.data.url, fileId: data.data.fileId, fileName: uploadTask.file.name, fileSize: uploadTask.file.size, fileType: uploadTask.file.type } }; if (uploadTask.onSuccess) { uploadTask.onSuccess(result); } resolve(result); } else { throw new Error(data.error || '上传失败'); } } catch (error) { reject(error); } }, fail: (error) => { console.error('❌ 上传请求失败:', error); if (uploadTask.onError) { uploadTask.onError(error); } reject(error); } }); // 监听上传进度 uploadTask_wx.onProgressUpdate((res) => { uploadTask.progress = res.progress; if (uploadTask.onProgress) { uploadTask.onProgress({ progress: res.progress, totalBytesSent: res.totalBytesSent, totalBytesExpectedToSend: res.totalBytesExpectedToSend }); } }); // 保存上传任务引用 uploadTask.wxTask = uploadTask_wx; }); } // 取消上传 cancelUpload(uploadId) { const uploadTask = this.currentUploads.get(uploadId); if (uploadTask && uploadTask.wxTask) { uploadTask.wxTask.abort(); this.currentUploads.delete(uploadId); } } // 生成上传ID generateUploadId() { return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // 📥 ===== 文件下载和缓存 ===== // 下载文件 async downloadFile(url, options = {}) { try { const { fileName, onProgress, useCache = true } = options; // 检查缓存 if (useCache) { const cachedPath = this.getCachedFilePath(url); if (cachedPath) { return { success: true, tempFilePath: cachedPath, cached: true }; } } // 执行下载 const result = await this.performDownload(url, { fileName, onProgress }); // 缓存文件 if (result.success && useCache) { this.cacheFile(url, result.tempFilePath); } return result; } catch (error) { console.error('❌ 下载文件失败:', error); return { success: false, error: error.message }; } } // 执行下载 async performDownload(url, options = {}) { return new Promise((resolve, reject) => { const downloadTask = wx.downloadFile({ url: url, success: (res) => { if (res.statusCode === 200) { resolve({ success: true, tempFilePath: res.tempFilePath, cached: false }); } else { reject(new Error(`下载失败: ${res.statusCode}`)); } }, fail: reject }); // 监听下载进度 if (options.onProgress) { downloadTask.onProgressUpdate((res) => { options.onProgress({ progress: res.progress, totalBytesWritten: res.totalBytesWritten, totalBytesExpectedToWrite: res.totalBytesExpectedToWrite }); }); } }); } // 检查存储权限 async checkStoragePermission() { try { const storageInfo = wx.getStorageInfoSync(); return true; } catch (error) { console.error('❌ 检查存储权限失败:', error); return false; } } // 加载缓存信息 async loadCacheInfo() { try { const cacheInfo = wx.getStorageSync('mediaCacheInfo') || {}; this.cacheStats = { totalSize: cacheInfo.totalSize || 0, fileCount: cacheInfo.fileCount || 0, lastCleanup: cacheInfo.lastCleanup || 0 }; } catch (error) { console.error('❌ 加载缓存信息失败:', error); } } // 保存缓存信息 async saveCacheInfo() { try { wx.setStorageSync('mediaCacheInfo', this.cacheStats); } catch (error) { console.error('❌ 保存缓存信息失败:', error); } } // 获取缓存文件路径 getCachedFilePath(url) { const cacheKey = this.generateCacheKey(url); return this.fileCache.get(cacheKey); } // 缓存文件 cacheFile(url, filePath) { const cacheKey = this.generateCacheKey(url); this.fileCache.set(cacheKey, filePath); // 更新缓存统计 this.cacheStats.fileCount++; this.saveCacheInfo(); } // 生成缓存键 generateCacheKey(url) { // 使用URL的hash作为缓存键 let hash = 0; for (let i = 0; i < url.length; i++) { const char = url.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // 转换为32位整数 } return `cache_${Math.abs(hash)}`; } // 启动缓存清理 startCacheCleanup() { setInterval(() => { this.performCacheCleanup(); }, this.mediaConfig.cache.cleanupInterval); } // 执行缓存清理 performCacheCleanup() { try { const now = Date.now(); const expireTime = this.mediaConfig.cache.expireTime; // 清理过期缓存 for (const [key, filePath] of this.fileCache) { try { const stats = wx.getFileInfo({ filePath }); if (now - stats.createTime > expireTime) { this.fileCache.delete(key); // 删除文件 wx.removeSavedFile({ filePath }); } } catch (error) { // 文件不存在,从缓存中移除 this.fileCache.delete(key); } } // 更新清理时间 this.cacheStats.lastCleanup = now; this.saveCacheInfo(); } catch (error) { console.error('❌ 缓存清理失败:', error); } } // 获取媒体管理器状态 getStatus() { return { isInitialized: this.isInitialized, uploadCount: this.currentUploads.size, cacheStats: { ...this.cacheStats }, config: this.mediaConfig }; } // 清除所有缓存 clearAllCache() { this.fileCache.clear(); this.cacheStats = { totalSize: 0, fileCount: 0, lastCleanup: Date.now() }; this.saveCacheInfo(); } // 重置管理器 reset() { // 取消所有上传任务 for (const [uploadId] of this.currentUploads) { this.cancelUpload(uploadId); } this.clearAllCache(); } } // 创建全局实例 const mediaManager = new MediaManager(); module.exports = mediaManager;