Uniapp文件命名实战:从时间戳到优雅自定义的完整方案

每次在Uniapp项目中处理文件下载,看到那一串毫无意义的数字时间戳文件名,是不是觉得特别别扭?上周我在开发一个企业文档管理系统时,就遇到了这个痛点——用户下载的合同、报表全是类似“1623456789012.docx”这样的文件名,不仅难以识别,还给后续的文件管理带来了巨大麻烦。更让人头疼的是,Uniapp官方文档中确实没有直接提供文件重命名的API,这让很多开发者只能无奈接受这种不友好的默认行为。

实际上,这个问题背后涉及的是Uniapp底层文件系统的操作逻辑。时间戳命名是uni.saveFile方法的默认行为,它确保了文件名的唯一性,但在实际业务场景中却严重影响了用户体验。经过多次实践和调试,我发现通过plus.io模块可以完美解决这个问题,而且方案比想象中要优雅得多。

本文面向有一定Uniapp开发经验的工程师,特别是那些需要处理本地文件存储、缓存管理、用户自定义文件命名等场景的开发者。无论你是开发文档管理应用、图片编辑工具,还是需要离线存储用户数据的应用,掌握文件重命名技巧都能让你的应用更加专业和用户友好。

1. 理解Uniapp的文件系统架构

在深入代码之前,我们需要先理清Uniapp如何处理本地文件。很多人对Uniapp的文件系统存在误解,认为它只是简单封装了H5的File API,实际上它的架构要复杂得多。

1.1 三种文件存储区域的差异

Uniapp将本地文件存储划分为三个主要区域,每个区域都有不同的访问权限和生命周期:

存储区域访问方式文件生命周期典型用途
应用私有目录plus.io.PRIVATE_WWW应用卸载时清除应用资源、配置文件
文档目录plus.io.DOCUMENTS永久存储用户文档、下载文件
缓存目录plus.io.CACHE系统可自动清理临时文件、缓存数据

当你使用uni.downloadFile下载文件时,默认会保存到文档目录。但这里有个关键细节:保存后的文件名会被自动转换为时间戳格式。这是系统为了保证文件名唯一性而采取的措施,但对于需要用户友好命名的场景来说,这显然不够理想。

1.2 时间戳命名的底层逻辑

为什么Uniapp要使用时间戳命名?这其实是一个设计权衡。考虑以下场景:

// 假设有两个用户同时下载同名文件
uni.downloadFile({
  url: 'https://example.com/report.docx',
  success: (res) => {
    uni.saveFile({
      tempFilePath: res.tempFilePath,
      success: (saveRes) => {
        console.log(saveRes.savedFilePath) // 输出类似: "_doc/1623456789012.docx"
      }
    })
  }
})

如果系统使用原始文件名“report.docx”,当用户多次下载同名文件时,就会产生覆盖问题。时间戳命名确保了每次下载都能生成唯一的文件标识,但代价是牺牲了可读性。

注意:虽然时间戳命名解决了冲突问题,但在实际业务中,我们往往需要更智能的命名策略,比如“合同_张三_20231201.docx”这样的格式。

2. 核心重命名方案:plus.io模块深度解析

既然官方没有提供直接的API,我们就需要深入底层,使用plus.io模块来实现文件重命名功能。这个模块是HTML5+扩展API的一部分,提供了更底层的文件系统操作能力。

2.1 基础重命名函数实现

让我们先看一个完整的重命名函数实现,然后逐步拆解每个部分:

/**
 * 重命名本地文件
 * @param {string} oldFilePath - 旧文件的完整路径(如:_doc/uniapp/1623456789012.docx)
 * @param {string} newFileName - 新文件名(必须包含后缀,如:contract.docx)
 * @returns {Promise<string>} 返回新文件的完整路径
 */
function renameFile(oldFilePath, newFileName) {
  return new Promise((resolve, reject) => {
    // 第一步:解析旧文件路径,获取文件Entry对象
    plus.io.resolveLocalFileSystemURL(oldFilePath, (entry) => {
      if (!entry) {
        reject(new Error('文件不存在或路径错误'));
        return;
      }
      
      // 第二步:获取父目录
      entry.getParent((parentEntry) => {
        // 第三步:执行移动/重命名操作
        entry.moveTo(parentEntry, newFileName, (newEntry) => {
          console.log('重命名成功,新路径:', newEntry.fullPath);
          resolve(newEntry.fullPath);
        }, (error) => {
          reject(new Error(`重命名失败: ${error.message}`));
        });
      }, (error) => {
        reject(new Error(`获取父目录失败: ${error.message}`));
      });
    }, (error) => {
      reject(new Error(`解析文件路径失败: ${error.message}`));
    });
  });
}

这个函数的核心是entry.moveTo()方法。虽然名为“移动”,但当目标目录与源目录相同时,实际上就实现了重命名操作。这里有几个关键点需要注意:

  • 路径格式:oldFilePath必须是完整路径,通常以_doc/、_www/或_documents/开头
  • 文件名要求:newFileName必须包含文件后缀,系统不会自动添加
  • 异步处理:所有文件操作都是异步的,需要使用回调或Promise

2.2 错误处理与边界情况

在实际使用中,我们需要考虑各种可能出现的异常情况。下面是一个增强版的错误处理方案:

async function safeRenameFile(oldFilePath, newFileName) {
  try {
    // 参数验证
    if (!oldFilePath || typeof oldFilePath !== 'string') {
      throw new Error('旧文件路径必须是字符串且不能为空');
    }
    
    if (!newFileName || typeof newFileName !== 'string') {
      throw new Error('新文件名必须是字符串且不能为空');
    }
    
    // 检查文件后缀
    if (!newFileName.includes('.')) {
      console.warn('新文件名未包含后缀,建议添加合适的文件扩展名');
    }
    
    // 检查文件是否存在
    const fileExists = await checkFileExists(oldFilePath);
    if (!fileExists) {
      throw new Error(`文件不存在: ${oldFilePath}`);
    }
    
    // 检查目标文件名是否已存在
    const parentPath = oldFilePath.substring(0, oldFilePath.lastIndexOf('/'));
    const newFilePath = `${parentPath}/${newFileName}`;
    const targetExists = await checkFileExists(newFilePath);
    
    if (targetExists) {
      // 处理文件名冲突
      const uniqueName = await generateUniqueName(newFileName, parentPath);
      console.log(`目标文件已存在,使用新名称: ${uniqueName}`);
      return await renameFile(oldFilePath, uniqueName);
    }
    
    // 执行重命名
    return await renameFile(oldFilePath, newFileName);
    
  } catch (error) {
    console.error('文件重命名失败:', error);
    // 根据错误类型提供用户友好的提示
    if (error.message.includes('权限')) {
      throw new Error('文件操作权限不足,请检查应用权限设置');
    } else if (error.message.includes('不存在')) {
      throw new Error('源文件不存在,可能已被删除或移动');
    } else {
      throw error;
    }
  }
}

// 辅助函数:检查文件是否存在
function checkFileExists(filePath) {
  return new Promise((resolve) => {
    plus.io.resolveLocalFileSystemURL(filePath, () => {
      resolve(true);
    }, () => {
      resolve(false);
    });
  });
}

// 辅助函数:生成唯一文件名
function generateUniqueName(baseName, directoryPath) {
  return new Promise(async (resolve) => {
    const nameParts = baseName.split('.');
    const extension = nameParts.length > 1 ? `.${nameParts.pop()}` : '';
    const base = nameParts.join('.');
    
    let counter = 1;
    let newName = `${base}${extension}`;
    
    while (true) {
      const testPath = `${directoryPath}/${newName}`;
      const exists = await checkFileExists(testPath);
      
      if (!exists) {
        resolve(newName);
        break;
      }
      
      newName = `${base}_${counter}${extension}`;
      counter++;
      
      // 防止无限循环
      if (counter > 1000) {
        // 使用时间戳确保唯一性
        const timestamp = Date.now();
        newName = `${base}_${timestamp}${extension}`;
        resolve(newName);
        break;
      }
    }
  });
}

这个增强版本处理了文件不存在、权限问题、文件名冲突等多种边界情况,确保了函数的健壮性。

3. 实战应用:与Uniapp原生API的集成

单纯的重命名函数还不够,我们需要将其与Uniapp的其他文件操作API无缝集成,形成完整的工作流。

3.1 结合uni.downloadFile的完整流程

下面是一个完整的文件下载并重命名的示例:

// 文件下载管理器
class FileDownloadManager {
  constructor() {
    this.downloadTasks = new Map();
  }
  
  /**
   * 下载文件并自定义命名
   * @param {Object} options 下载选项
   * @param {string} options.url 文件URL
   * @param {string} options.fileName 自定义文件名
   * @param {Function} options.onProgress 进度回调
   * @returns {Promise<string>} 返回最终文件路径
   */
  async downloadWithCustomName(options) {
    const { url, fileName, onProgress } = options;
    
    try {
      // 步骤1:下载文件到临时路径
      console.log('开始下载文件:', url);
      const downloadResult = await this._downloadFile(url, onProgress);
      
      // 步骤2:保存到本地(此时会得到时间戳命名的文件)
      const savedPath = await this._saveFile(downloadResult.tempFilePath);
      console.log('文件已保存到:', savedPath);
      
      // 步骤3:重命名为自定义名称
      const finalPath = await this._renameToCustomName(savedPath, fileName);
      console.log('文件重命名完成:', finalPath);
      
      return finalPath;
      
    } catch (error) {
      console.error('下载过程出错:', error);
      throw this._formatError(error);
    }
  }
  
  // 私有方法:执行下载
  _downloadFile(url, onProgress) {
    return new Promise((resolve, reject) => {
      const downloadTask = uni.downloadFile({
        url,
        success: resolve,
        fail: reject
      });
      
      // 监听下载进度
      if (onProgress && typeof onProgress === 'function') {
        downloadTask.onProgressUpdate((res) => {
          onProgress({
            progress: res.progress,
            totalBytesWritten: res.totalBytesWritten,
            totalBytesExpectedToWrite: res.totalBytesExpectedToWrite
          });
        });
      }
      
      // 保存任务引用,便于后续管理
      const taskId = Date.now();
      this.downloadTasks.set(taskId, downloadTask);
      
      // 下载完成后清理任务引用
      downloadTask.then(() => {
        this.downloadTasks.delete(taskId);
      }).catch(() => {
        this.downloadTasks.delete(taskId);
      });
    });
  }
  
  // 私有方法:保存文件
  _saveFile(tempFilePath) {
    return new Promise((resolve, reject) => {
      uni.saveFile({
        tempFilePath,
        success: (res) => resolve(res.savedFilePath),
        fail: reject
      });
    });
  }
  
  // 私有方法:重命名文件
  async _renameToCustomName(savedFilePath, customFileName) {
    // 确保文件名包含后缀
    let finalFileName = customFileName;
    if (!finalFileName.includes('.')) {
      // 尝试从URL或原文件路径中提取后缀
      const originalExt = this._extractFileExtension(savedFilePath);
      if (originalExt) {
        finalFileName = `${customFileName}.${originalExt}`;
      }
    }
    
    return await safeRenameFile(savedFilePath, finalFileName);
  }
  
  // 辅助方法:提取文件后缀
  _extractFileExtension(filePath) {
    const matches = filePath.match(/\.([0-9a-z]+)$/i);
    return matches ? matches[1] : null;
  }
  
  // 错误格式化
  _formatError(error) {
    const errorMap = {
      'fail file not exists': '文件不存在',
      'fail permission denied': '权限不足',
      'fail save file failed': '文件保存失败'
    };
    
    const message = errorMap[error.errMsg] || error.message || '操作失败';
    return new Error(message);
  }
  
  // 取消所有下载任务
  cancelAllDownloads() {
    this.downloadTasks.forEach(task => {
      try {
        task.abort();
      } catch (e) {
        console.warn('取消下载任务时出错:', e);
      }
    });
    this.downloadTasks.clear();
  }
}

// 使用示例
const downloadManager = new FileDownloadManager();

// 下载并重命名文件
async function downloadContract() {
  try {
    const finalPath = await downloadManager.downloadWithCustomName({
      url: 'https://api.example.com/contracts/123',
      fileName: '采购合同_20231201',
      onProgress: (progress) => {
        console.log(`下载进度: ${progress.progress}%`);
        // 可以更新UI进度条
        uni.showLoading({
          title: `下载中 ${progress.progress}%`,
          mask: true
        });
      }
    });
    
    uni.hideLoading();
    uni.showToast({
      title: '文件下载完成',
      icon: 'success'
    });
    
    console.log('最终文件路径:', finalPath);
    return finalPath;
    
  } catch (error) {
    uni.hideLoading();
    uni.showToast({
      title: error.message,
      icon: 'none'
    });
    throw error;
  }
}

这个管理器类封装了完整的下载、保存、重命名流程,并提供了进度监控、错误处理和任务管理功能。

3.2 与uni.getSavedFileList的配合使用

在实际应用中,我们经常需要管理已保存的文件列表。uni.getSavedFileList可以获取所有通过uni.saveFile保存的文件,结合重命名功能,可以实现强大的文件管理能力。

// 文件管理系统
class FileManager {
  constructor() {
    this.fileList = [];
  }
  
  // 刷新文件列表
  async refreshFileList() {
    try {
      const result = await new Promise((resolve, reject) => {
        uni.getSavedFileList({
          success: resolve,
          fail: reject
        });
      });
      
      this.fileList = result.fileList || [];
      console.log('当前保存的文件列表:', this.fileList);
      return this.fileList;
      
    } catch (error) {
      console.error('获取文件列表失败:', error);
      throw new Error('无法获取文件列表,请检查权限设置');
    }
  }
  
  // 批量重命名文件
  async batchRenameFiles(renameRules) {
    const results = [];
    
    for (const rule of renameRules) {
      try {
        const { filePath, newName } = rule;
        
        // 验证文件是否存在
        const fileExists = this.fileList.some(file => file.filePath === filePath);
        if (!fileExists) {
          results.push({
            filePath,
            success: false,
            error: '文件不存在于保存列表中'
          });
          continue;
        }
        
        // 执行重命名
        const newPath = await safeRenameFile(filePath, newName);
        
        results.push({
          filePath,
          newPath,
          success: true
        });
        
        // 更新本地文件列表
        const fileIndex = this.fileList.findIndex(f => f.filePath === filePath);
        if (fileIndex > -1) {
          this.fileList[fileIndex].filePath = newPath;
          this.fileList[fileIndex].fileName = newName;
        }
        
      } catch (error) {
        results.push({
          filePath: rule.filePath,
          success: false,
          error: error.message
        });
      }
    }
    
    return results;
  }
  
  // 根据规则自动重命名
  async autoRenameByPattern(pattern) {
    const renameRules = [];
    
    for (const file of this.fileList) {
      // 从文件路径中提取原始时间戳
      const timestampMatch = file.filePath.match(/(\d{10,})/);
      const timestamp = timestampMatch ? parseInt(timestampMatch[1]) : Date.now();
      
      // 根据时间戳生成可读日期
      const date = new Date(timestamp);
      const dateStr = `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
      
      // 提取可能的文件类型
      const extMatch = file.filePath.match(/\.([a-zA-Z0-9]+)$/);
      const extension = extMatch ? extMatch[1] : 'file';
      
      // 应用命名模式
      let newName;
      switch (pattern) {
        case 'date':
          newName = `文件_${dateStr}.${extension}`;
          break;
        case 'datetime':
          const timeStr = `${date.getHours().toString().padStart(2, '0')}${date.getMinutes().toString().padStart(2, '0')}`;
          newName = `文件_${date.getMonth() + 1}${date.getDate()}_${timeStr}.${extension}`;
          break;
        case 'sequential':
          // 需要先获取当前序号
          const seq = this._getNextSequenceNumber(extension);
          newName = `文档_${seq.toString().padStart(3, '0')}.${extension}`;
          break;
        default:
          newName = `重命名文件.${extension}`;
      }
      
      renameRules.push({
        filePath: file.filePath,
        newName
      });
    }
    
    return await this.batchRenameFiles(renameRules);
  }
  
  // 获取下一个序列号
  _getNextSequenceNumber(extension) {
    const sameTypeFiles = this.fileList.filter(f => 
      f.filePath.endsWith(`.${extension}`)
    );
    return sameTypeFiles.length + 1;
  }
  
  // 搜索并重命名
  async searchAndRename(searchTerm, newNamePattern) {
    const matchedFiles = this.fileList.filter(file => {
      // 这里可以根据实际需求实现更复杂的搜索逻辑
      return file.filePath.includes(searchTerm);
    });
    
    const renameRules = matchedFiles.map((file, index) => {
      const extMatch = file.filePath.match(/\.([a-zA-Z0-9]+)$/);
      const extension = extMatch ? extMatch[1] : '';
      
      return {
        filePath: file.filePath,
        newName: newNamePattern
          .replace('{index}', (index + 1).toString().padStart(3, '0'))
          .replace('{ext}', extension)
      };
    });
    
    return await this.batchRenameFiles(renameRules);
  }
}

// 使用示例
async function manageDownloadedFiles() {
  const fileManager = new FileManager();
  
  // 1. 刷新文件列表
  await fileManager.refreshFileList();
  
  // 2. 自动按日期重命名所有文件
  const autoRenameResult = await fileManager.autoRenameByPattern('date');
  console.log('自动重命名结果:', autoRenameResult);
  
  // 3. 搜索特定文件并重命名
  const searchResult = await fileManager.searchAndRename(
    'contract',
    '合同文件_{index}.{ext}'
  );
  console.log('搜索重命名结果:', searchResult);
  
  // 4. 显示最终文件列表
  const finalList = await fileManager.refreshFileList();
  console.log('最终文件列表:', finalList);
  
  return finalList;
}

4. 高级技巧与性能优化

掌握了基础的重命名操作后,让我们深入一些高级技巧和性能优化策略。

4.1 大文件处理与内存管理

当处理大型文件(如视频、高清图片)时,直接操作可能会遇到内存问题。以下是一些优化策略:

// 分块处理大文件的重命名
class LargeFileHandler {
  constructor() {
    this.chunkSize = 1024 * 1024; // 1MB chunks
    this.maxRetries = 3;
  }
  
  async renameLargeFile(oldPath, newName, onProgress) {
    // 先检查文件大小
    const fileInfo = await this.getFileInfo(oldPath);
    
    if (fileInfo.size < 10 * 1024 * 1024) { // 小于10MB直接处理
      return await safeRenameFile(oldPath, newName);
    }
    
    // 大文件使用分块复制+删除策略
    console.log(`检测到大文件: ${fileInfo.size} bytes,使用分块处理`);
    
    const parentDir = oldPath.substring(0, oldPath.lastIndexOf('/'));
    const tempPath = `${parentDir}/temp_${Date.now()}_${newName}`;
    
    try {
      // 创建新文件
      await this.createEmptyFile(tempPath);
      
      // 分块复制
      await this.copyFileInChunks(oldPath, tempPath, onProgress);
      
      // 验证文件完整性
      const newFileInfo = await this.getFileInfo(tempPath);
      if (newFileInfo.size !== fileInfo.size) {
        throw new Error('文件复制后大小不一致');
      }
      
      // 删除原文件
      await this.deleteFile(oldPath);
      
      // 重命名临时文件
      await safeRenameFile(tempPath, newName);
      
      const finalPath = `${parentDir}/${newName}`;
      console.log('大文件重命名完成:', finalPath);
      return finalPath;
      
    } catch (error) {
      // 清理临时文件
      await this.cleanupTempFile(tempPath);
      throw error;
    }
  }
  
  async getFileInfo(filePath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
        entry.getMetadata((metadata) => {
          resolve({
            size: metadata.size,
            modified: metadata.modificationTime
          });
        }, reject);
      }, reject);
    });
  }
  
  async createEmptyFile(filePath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        filePath.substring(0, filePath.lastIndexOf('/')),
        (dirEntry) => {
          dirEntry.getFile(
            filePath.substring(filePath.lastIndexOf('/') + 1),
            { create: true, exclusive: false },
            resolve,
            reject
          );
        },
        reject
      );
    });
  }
  
  async copyFileInChunks(sourcePath, destPath, onProgress) {
    // 这里简化实现,实际需要更复杂的分块读写逻辑
    // 可以使用FileReader和FileWriter进行分块操作
    
    console.log('开始分块复制文件...');
    // 实际实现会根据文件系统API进行调整
    // 这里展示的是概念性代码
    
    if (onProgress) {
      let progress = 0;
      const interval = setInterval(() => {
        progress += 10;
        if (progress >= 100) {
          clearInterval(interval);
          onProgress({ progress: 100 });
        } else {
          onProgress({ progress });
        }
      }, 100);
    }
    
    // 模拟复制完成
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  
  async deleteFile(filePath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
        entry.remove(resolve, reject);
      }, reject);
    });
  }
  
  async cleanupTempFile(filePath) {
    try {
      await this.deleteFile(filePath);
    } catch (error) {
      console.warn('清理临时文件失败:', error);
    }
  }
}

4.2 批量操作的性能优化

当需要重命名大量文件时,需要考虑性能问题:

// 批量重命名优化器
class BatchRenameOptimizer {
  constructor(maxConcurrent = 3) {
    this.maxConcurrent = maxConcurrent; // 最大并发数
    this.activeTasks = 0;
    this.queue = [];
    this.results = [];
  }
  
  // 添加重命名任务到队列
  addRenameTask(oldPath, newName) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        oldPath,
        newName,
        resolve,
        reject
      });
      this._processQueue();
    });
  }
  
  // 处理队列
  async _processQueue() {
    if (this.activeTasks >= this.maxConcurrent || this.queue.length === 0) {
      return;
    }
    
    this.activeTasks++;
    const task = this.queue.shift();
    
    try {
      const result = await this._executeRename(task.oldPath, task.newName);
      this.results.push({ ...task, success: true, result });
      task.resolve(result);
    } catch (error) {
      this.results.push({ ...task, success: false, error: error.message });
      task.reject(error);
    } finally {
      this.activeTasks--;
      this._processQueue(); // 处理下一个任务
    }
  }
  
  async _executeRename(oldPath, newName) {
    // 这里可以添加重试逻辑
    let lastError;
    
    for (let attempt = 1; attempt <= 3; attempt++) {
      try {
        return await safeRenameFile(oldPath, newName);
      } catch (error) {
        lastError = error;
        console.warn(`重命名尝试 ${attempt} 失败:`, error.message);
        
        if (attempt < 3) {
          // 等待指数退避时间
          await new Promise(resolve => 
            setTimeout(resolve, Math.pow(2, attempt) * 100)
          );
        }
      }
    }
    
    throw lastError;
  }
  
  // 获取所有结果
  getAllResults() {
    return this.results;
  }
  
  // 等待所有任务完成
  async waitForCompletion() {
    while (this.queue.length > 0 || this.activeTasks > 0) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    return this.results;
  }
}

// 使用示例:批量重命名性能优化
async function optimizeBatchRename(fileList) {
  const optimizer = new BatchRenameOptimizer(5); // 最多5个并发
  
  const promises = fileList.map(file => 
    optimizer.addRenameTask(file.oldPath, file.newName)
  );
  
  // 可以添加进度监控
  let completed = 0;
  const total = fileList.length;
  
  const progressInterval = setInterval(() => {
    completed = optimizer.results.length;
    const progress = Math.round((completed / total) * 100);
    console.log(`批量重命名进度: ${progress}% (${completed}/${total})`);
    
    if (completed === total) {
      clearInterval(progressInterval);
    }
  }, 500);
  
  try {
    const results = await Promise.allSettled(promises);
    clearInterval(progressInterval);
    
    const successCount = results.filter(r => r.status === 'fulfilled').length;
    console.log(`批量重命名完成: ${successCount}/${total} 成功`);
    
    return {
      success: successCount,
      failed: total - successCount,
      details: optimizer.getAllResults()
    };
    
  } catch (error) {
    clearInterval(progressInterval);
    throw error;
  }
}

4.3 文件命名策略与最佳实践

在实际项目中,一个好的命名策略可以大大提升文件管理的效率。以下是一些实用的命名模式:

// 智能文件命名生成器
class SmartFileNameGenerator {
  constructor() {
    this.patterns = {
      // 日期时间模式
      date: {
        template: '{YYYY}{MM}{DD}_{HH}{mm}{ss}',
        generate: (params) => {
          const now = params.date || new Date();
          return this._formatDate(now, this.patterns.date.template);
        }
      },
      
      // 序列号模式
      sequence: {
        template: '文件_{SEQ3}',
        generate: (params) => {
          const seq = params.sequence || 1;
          const padded = seq.toString().padStart(3, '0');
          return this.patterns.sequence.template.replace('{SEQ3}', padded);
        }
      },
      
      // 业务相关模式
      business: {
        template: '{TYPE}_{USER}_{DATE}',
        generate: (params) => {
          const type = params.type || '文档';
          const user = params.user || '未知用户';
          const date = this._formatDate(params.date || new Date(), '{YYYY}{MM}{DD}');
          return `${type}_${user}_${date}`;
        }
      },
      
      // 哈希模式(避免冲突)
      hash: {
        template: 'file_{HASH8}',
        generate: (params) => {
          const content = params.content || Math.random().toString();
          const hash = this._simpleHash(content).substring(0, 8);
          return this.patterns.hash.template.replace('{HASH8}', hash);
        }
      }
    };
  }
  
  // 生成文件名
  generateFileName(patternName, params = {}) {
    const pattern = this.patterns[patternName];
    if (!pattern) {
      throw new Error(`未知的命名模式: ${patternName}`);
    }
    
    let fileName = pattern.generate(params);
    
    // 添加文件扩展名
    if (params.extension) {
      if (!fileName.includes('.')) {
        fileName += `.${params.extension}`;
      }
    }
    
    // 清理非法字符
    fileName = this._sanitizeFileName(fileName);
    
    return fileName;
  }
  
  // 批量生成不重复的文件名
  generateUniqueNames(patternName, count, baseParams = {}) {
    const names = new Set();
    const results = [];
    
    for (let i = 0; i < count; i++) {
      let attempts = 0;
      let name;
      
      do {
        const params = { ...baseParams, sequence: i + 1 };
        name = this.generateFileName(patternName, params);
        
        // 如果名称重复,添加随机后缀
        if (names.has(name) && attempts < 10) {
          const randomSuffix = Math.random().toString(36).substring(2, 6);
          name = name.replace(/(\.[^\.]+)?$/, `_${randomSuffix}$1`);
          attempts++;
        }
      } while (names.has(name) && attempts < 10);
      
      // 如果还是重复,使用时间戳
      if (names.has(name)) {
        const timestamp = Date.now().toString().slice(-6);
        name = name.replace(/(\.[^\.]+)?$/, `_${timestamp}$1`);
      }
      
      names.add(name);
      results.push(name);
    }
    
    return results;
  }
  
  // 日期格式化
  _formatDate(date, format) {
    const pad = (n) => n.toString().padStart(2, '0');
    
    const replacements = {
      '{YYYY}': date.getFullYear(),
      '{YY}': date.getFullYear().toString().slice(-2),
      '{MM}': pad(date.getMonth() + 1),
      '{DD}': pad(date.getDate()),
      '{HH}': pad(date.getHours()),
      '{mm}': pad(date.getMinutes()),
      '{ss}': pad(date.getSeconds())
    };
    
    let result = format;
    for (const [key, value] of Object.entries(replacements)) {
      result = result.replace(new RegExp(key, 'g'), value);
    }
    
    return result;
  }
  
  // 简单哈希函数
  _simpleHash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return Math.abs(hash).toString(36);
  }
  
  // 清理文件名中的非法字符
  _sanitizeFileName(name) {
    // 移除或替换非法字符
    return name
      .replace(/[<>:"/\\|?*]/g, '_')  // Windows非法字符
      .replace(/[\x00-\x1F\x7F]/g, '') // 控制字符
      .replace(/^\.+/, '')             // 不能以点开头
      .replace(/\s+/g, ' ')            // 合并多个空格
      .trim();
  }
}

// 使用示例
async function implementSmartNaming() {
  const generator = new SmartFileNameGenerator();
  
  // 场景1:为下载的图片生成有意义的名称
  const imageNames = generator.generateUniqueNames('business', 5, {
    type: '产品图片',
    user: '销售部',
    extension: 'jpg'
  });
  
  console.log('生成的图片文件名:', imageNames);
  // 输出: ["产品图片_销售部_20231201.jpg", "产品图片_销售部_20231201_1.jpg", ...]
  
  // 场景2:生成按日期时间命名的日志文件
  const logName = generator.generateFileName('date', {
    date: new Date(),
    extension: 'log'
  });
  
  console.log('日志文件名:', logName);
  // 输出: "20231201_143025.log"
  
  // 场景3:批量处理用户上传的文件
  const userFiles = [
    { originalName: 'document1.pdf', type: '合同' },
    { originalName: 'scan.jpg', type: '身份证' },
    { originalName: 'proof.png', type: '证明文件' }
  ];
  
  const renamedFiles = userFiles.map((file, index) => {
    const extension = file.originalName.split('.').pop();
    const newName = generator.generateFileName('business', {
      type: file.type,
      user: '张三',
      date: new Date(),
      extension
    });
    
    return {
      original: file.originalName,
      newName,
      extension
    };
  });
  
  console.log('用户文件重命名方案:', renamedFiles);
  
  return renamedFiles;
}

在实际项目中使用这些方案时,我发现最关键的不仅是技术实现,更是对用户场景的深入理解。比如在文档管理系统中,用户可能更倾向于“合同_客户名_日期”这样的命名格式,而在图片管理应用中,按拍摄时间自动分组命名可能更实用。真正好的文件命名方案应该是可配置、可扩展的,能够适应不同业务需求的变化。

另一个重要的经验是错误处理的完备性。文件操作失败的原因多种多样——权限不足、存储空间不够、文件名冲突、甚至是用户中途取消操作。我们的代码需要能够优雅地处理所有这些情况,给用户清晰的反馈,而不是让应用崩溃或陷入不可预知的状态。

最后关于性能,特别是在移动设备上,文件操作需要格外小心。我遇到过因为一次性重命名太多文件导致应用卡顿的情况,后来通过引入队列控制和进度反馈才解决。现在这套方案在多个实际项目中运行稳定,能够处理从几个到几百个文件的批量重命名需求。

更多推荐