Files
jiebanke/docs/文件上传系统文档.md

17 KiB
Raw Blame History

文件上传系统文档

概述

文件上传系统为解班客平台提供了完整的文件管理功能,支持多种文件类型的上传、处理、存储和管理。系统采用模块化设计,支持图片处理、文件验证、安全控制等功能。

系统架构

核心组件

  1. 上传中间件 (middleware/upload.js)

    • 文件上传处理
    • 文件类型验证
    • 大小限制控制
    • 存储路径管理
  2. 图片处理器

    • 图片压缩和格式转换
    • 缩略图生成
    • 尺寸调整
    • 质量优化
  3. 文件管理控制器 (controllers/admin/fileManagement.js)

    • 文件列表管理
    • 文件统计分析
    • 批量操作
    • 清理功能
  4. 错误处理机制

    • 统一错误响应
    • 详细错误日志
    • 安全错误信息

支持的文件类型

图片文件

  • 格式: JPG, JPEG, PNG, GIF, WebP
  • 用途: 头像、动物图片、旅行照片
  • 处理: 自动压缩、生成缩略图、格式转换

文档文件

  • 格式: PDF, DOC, DOCX, XLS, XLSX, TXT
  • 用途: 证书、合同、报告等
  • 处理: 文件验证、病毒扫描(计划中)

文件分类存储

存储目录结构

uploads/
├── avatars/          # 用户头像
├── animals/          # 动物图片
├── travels/          # 旅行图片
├── documents/        # 文档文件
└── temp/            # 临时文件

文件命名规则

  • 格式: {timestamp}_{randomString}.{extension}
  • 示例: 1701234567890_a1b2c3d4.jpg
  • 优势: 避免重名、便于排序、安全性高

上传限制配置

头像上传

  • 文件类型: JPG, PNG
  • 文件大小: 最大 2MB
  • 文件数量: 1个
  • 处理: 300x300像素生成100x100缩略图

动物图片上传

  • 文件类型: JPG, PNG, GIF, WebP
  • 文件大小: 最大 5MB
  • 文件数量: 最多 5张
  • 处理: 800x600像素生成200x200缩略图

旅行图片上传

  • 文件类型: JPG, PNG, GIF, WebP
  • 文件大小: 最大 5MB
  • 文件数量: 最多 10张
  • 处理: 1200x800像素生成300x300缩略图

文档上传

  • 文件类型: PDF, DOC, DOCX, XLS, XLSX, TXT
  • 文件大小: 最大 10MB
  • 文件数量: 最多 3个
  • 处理: 仅验证,不做格式转换

API接口说明

文件上传接口

1. 头像上传

POST /api/v1/admin/files/upload/avatar
Content-Type: multipart/form-data

avatar: [文件]

2. 动物图片上传

POST /api/v1/admin/files/upload/animal
Content-Type: multipart/form-data

images: [文件1, 文件2, ...]

3. 旅行图片上传

POST /api/v1/admin/files/upload/travel
Content-Type: multipart/form-data

images: [文件1, 文件2, ...]

4. 文档上传

POST /api/v1/admin/files/upload/document
Content-Type: multipart/form-data

documents: [文件1, 文件2, ...]

文件管理接口

1. 获取文件列表

GET /api/v1/admin/files?page=1&limit=20&type=all&keyword=搜索词

2. 获取文件详情

GET /api/v1/admin/files/{file_id}

3. 删除文件

DELETE /api/v1/admin/files/{file_id}

4. 批量删除文件

POST /api/v1/admin/files/batch/delete
Content-Type: application/json

{
  "file_ids": ["id1", "id2", "id3"]
}

5. 获取文件统计

GET /api/v1/admin/files/statistics

6. 清理无用文件

POST /api/v1/admin/files/cleanup?dry_run=true

图片处理功能

自动处理流程

  1. 上传验证: 检查文件类型、大小、数量
  2. 格式转换: 统一转换为JPEG格式可配置
  3. 尺寸调整: 按预设尺寸调整图片大小
  4. 质量压缩: 优化文件大小,保持视觉质量
  5. 缩略图生成: 生成小尺寸预览图
  6. 文件保存: 保存到指定目录

处理参数配置

// 头像处理配置
{
  width: 300,
  height: 300,
  quality: 85,
  format: 'jpeg',
  thumbnail: true,
  thumbnailSize: 100
}

// 动物图片处理配置
{
  width: 800,
  height: 600,
  quality: 80,
  format: 'jpeg',
  thumbnail: true,
  thumbnailSize: 200
}

安全机制

文件验证

  1. MIME类型检查: 验证文件真实类型
  2. 文件扩展名检查: 防止恶意文件上传
  3. 文件大小限制: 防止大文件攻击
  4. 文件数量限制: 防止批量上传攻击

存储安全

  1. 随机文件名: 防止文件名猜测
  2. 目录隔离: 不同类型文件分目录存储
  3. 访问控制: 通过Web服务器配置访问权限
  4. 定期清理: 自动清理临时和无用文件

错误处理

  1. 统一错误格式: 标准化错误响应
  2. 详细日志记录: 记录所有操作和错误
  3. 安全错误信息: 不暴露系统内部信息
  4. 异常恢复: 上传失败时自动清理临时文件

性能优化

图片优化

  1. 智能压缩: 根据图片内容调整压缩参数
  2. 格式选择: 自动选择最优图片格式
  3. 渐进式JPEG: 支持渐进式加载
  4. WebP支持: 现代浏览器使用WebP格式

存储优化

  1. 分目录存储: 避免单目录文件过多
  2. CDN集成: 支持CDN加速计划中
  3. 缓存策略: 合理设置HTTP缓存头
  4. 压缩传输: 启用gzip压缩

并发处理

  1. 异步处理: 图片处理使用异步操作
  2. 队列机制: 大批量操作使用队列(计划中)
  3. 限流控制: 防止并发上传过多
  4. 资源监控: 监控CPU和内存使用

监控和统计

文件统计

  • 总文件数量: 系统中所有文件的数量
  • 存储空间使用: 各类型文件占用的存储空间
  • 文件格式分布: 不同格式文件的数量和占比
  • 上传趋势: 文件上传的时间趋势

性能监控

  • 上传成功率: 文件上传的成功率统计
  • 处理时间: 文件处理的平均时间
  • 错误率: 各类错误的发生频率
  • 存储使用率: 存储空间的使用情况

日志记录

  • 操作日志: 记录所有文件操作
  • 错误日志: 记录所有错误和异常
  • 性能日志: 记录性能相关数据
  • 安全日志: 记录安全相关事件

维护和管理

定期维护任务

  1. 清理临时文件: 每小时清理超过24小时的临时文件
  2. 清理无用文件: 定期扫描和清理不再使用的文件
  3. 日志轮转: 定期归档和清理日志文件
  4. 存储空间监控: 监控存储空间使用情况

备份策略

  1. 增量备份: 每日增量备份新上传的文件
  2. 全量备份: 每周全量备份所有文件
  3. 异地备份: 重要文件异地备份(计划中)
  4. 恢复测试: 定期测试备份恢复功能

故障处理

  1. 自动恢复: 临时故障自动重试
  2. 降级服务: 服务异常时提供基础功能
  3. 故障通知: 严重故障及时通知管理员
  4. 快速恢复: 提供快速故障恢复方案

使用示例

前端上传示例

HTML表单上传

<form id="uploadForm" enctype="multipart/form-data">
  <input type="file" name="images" multiple accept="image/*">
  <button type="submit">上传</button>
</form>

JavaScript上传

async function uploadFiles(files, type = 'animal') {
  const formData = new FormData();
  
  // 添加文件到表单数据
  for (let i = 0; i < files.length; i++) {
    formData.append('images', files[i]);
  }
  
  try {
    const response = await fetch(`/api/v1/admin/files/upload/${type}`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`
      },
      body: formData
    });
    
    const result = await response.json();
    
    if (result.success) {
      console.log('上传成功:', result.data.files);
      return result.data.files;
    } else {
      throw new Error(result.message);
    }
  } catch (error) {
    console.error('上传失败:', error);
    throw error;
  }
}

// 使用示例
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
  const files = e.target.files;
  if (files.length > 0) {
    try {
      const uploadedFiles = await uploadFiles(files, 'animal');
      // 处理上传成功的文件
      displayUploadedFiles(uploadedFiles);
    } catch (error) {
      // 处理上传错误
      showError(error.message);
    }
  }
});

Vue.js组件示例

<template>
  <div class="file-upload">
    <div class="upload-area" @drop="handleDrop" @dragover.prevent>
      <input 
        ref="fileInput" 
        type="file" 
        multiple 
        :accept="acceptTypes"
        @change="handleFileSelect"
        style="display: none"
      >
      <button @click="$refs.fileInput.click()">选择文件</button>
      <p>或拖拽文件到此处</p>
    </div>
    
    <div v-if="uploading" class="upload-progress">
      <div class="progress-bar">
        <div class="progress-fill" :style="{width: progress + '%'}"></div>
      </div>
      <p>上传中... {{ progress }}%</p>
    </div>
    
    <div v-if="uploadedFiles.length > 0" class="uploaded-files">
      <h3>已上传文件</h3>
      <div class="file-list">
        <div v-for="file in uploadedFiles" :key="file.id" class="file-item">
          <img v-if="file.thumbnailUrl" :src="file.thumbnailUrl" :alt="file.filename">
          <div class="file-info">
            <p class="filename">{{ file.originalName }}</p>
            <p class="filesize">{{ formatFileSize(file.size) }}</p>
          </div>
          <button @click="deleteFile(file.id)" class="delete-btn">删除</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'FileUpload',
  props: {
    uploadType: {
      type: String,
      default: 'animal'
    },
    maxFiles: {
      type: Number,
      default: 5
    }
  },
  data() {
    return {
      uploading: false,
      progress: 0,
      uploadedFiles: []
    };
  },
  computed: {
    acceptTypes() {
      const types = {
        avatar: 'image/jpeg,image/png',
        animal: 'image/jpeg,image/png,image/gif,image/webp',
        travel: 'image/jpeg,image/png,image/gif,image/webp',
        document: '.pdf,.doc,.docx,.xls,.xlsx,.txt'
      };
      return types[this.uploadType] || 'image/*';
    }
  },
  methods: {
    handleFileSelect(event) {
      const files = Array.from(event.target.files);
      this.uploadFiles(files);
    },
    
    handleDrop(event) {
      event.preventDefault();
      const files = Array.from(event.dataTransfer.files);
      this.uploadFiles(files);
    },
    
    async uploadFiles(files) {
      if (files.length === 0) return;
      
      if (files.length > this.maxFiles) {
        this.$message.error(`最多只能上传${this.maxFiles}个文件`);
        return;
      }
      
      this.uploading = true;
      this.progress = 0;
      
      const formData = new FormData();
      const fieldName = this.uploadType === 'avatar' ? 'avatar' : 
                       this.uploadType === 'document' ? 'documents' : 'images';
      
      files.forEach(file => {
        formData.append(fieldName, file);
      });
      
      try {
        const response = await this.$http.post(
          `/admin/files/upload/${this.uploadType}`,
          formData,
          {
            headers: {
              'Content-Type': 'multipart/form-data'
            },
            onUploadProgress: (progressEvent) => {
              this.progress = Math.round(
                (progressEvent.loaded * 100) / progressEvent.total
              );
            }
          }
        );
        
        if (response.data.success) {
          this.uploadedFiles.push(...response.data.data.files);
          this.$message.success('文件上传成功');
          this.$emit('uploaded', response.data.data.files);
        } else {
          throw new Error(response.data.message);
        }
      } catch (error) {
        console.error('上传失败:', error);
        this.$message.error(error.message || '文件上传失败');
      } finally {
        this.uploading = false;
        this.progress = 0;
      }
    },
    
    async deleteFile(fileId) {
      try {
        const response = await this.$http.delete(`/admin/files/${fileId}`);
        
        if (response.data.success) {
          this.uploadedFiles = this.uploadedFiles.filter(f => f.id !== fileId);
          this.$message.success('文件删除成功');
          this.$emit('deleted', fileId);
        } else {
          throw new Error(response.data.message);
        }
      } catch (error) {
        console.error('删除失败:', error);
        this.$message.error(error.message || '文件删除失败');
      }
    },
    
    formatFileSize(bytes) {
      if (bytes === 0) return '0 Bytes';
      const k = 1024;
      const sizes = ['Bytes', 'KB', 'MB', 'GB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }
  }
};
</script>

<style scoped>
.file-upload {
  max-width: 600px;
  margin: 0 auto;
}

.upload-area {
  border: 2px dashed #ddd;
  border-radius: 8px;
  padding: 40px;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.3s;
}

.upload-area:hover {
  border-color: #007bff;
}

.upload-progress {
  margin: 20px 0;
}

.progress-bar {
  width: 100%;
  height: 20px;
  background-color: #f0f0f0;
  border-radius: 10px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background-color: #007bff;
  transition: width 0.3s;
}

.uploaded-files {
  margin-top: 20px;
}

.file-list {
  display: grid;
  gap: 10px;
}

.file-item {
  display: flex;
  align-items: center;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.file-item img {
  width: 50px;
  height: 50px;
  object-fit: cover;
  border-radius: 4px;
  margin-right: 10px;
}

.file-info {
  flex: 1;
}

.filename {
  font-weight: bold;
  margin: 0 0 5px 0;
}

.filesize {
  color: #666;
  margin: 0;
  font-size: 12px;
}

.delete-btn {
  background-color: #dc3545;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
}

.delete-btn:hover {
  background-color: #c82333;
}
</style>

故障排除

常见问题

1. 上传失败

问题: 文件上传时返回错误 可能原因:

  • 文件大小超出限制
  • 文件类型不支持
  • 服务器存储空间不足
  • 网络连接问题

解决方案:

  • 检查文件大小和类型
  • 确认服务器存储空间
  • 检查网络连接
  • 查看错误日志

2. 图片处理失败

问题: 图片上传成功但处理失败 可能原因:

  • Sharp库未正确安装
  • 图片文件损坏
  • 服务器内存不足
  • 权限问题

解决方案:

  • 重新安装Sharp库
  • 检查图片文件完整性
  • 增加服务器内存
  • 检查文件权限

3. 文件访问404

问题: 上传的文件无法访问 可能原因:

  • 静态文件服务未配置
  • 文件路径错误
  • 文件被误删
  • 权限设置问题

解决方案:

  • 配置静态文件服务
  • 检查文件路径
  • 恢复备份文件
  • 调整文件权限

调试方法

1. 启用详细日志

// 在环境变量中设置
NODE_ENV=development
LOG_LEVEL=debug

2. 检查上传目录权限

# 检查目录权限
ls -la uploads/

# 设置正确权限
chmod 755 uploads/
chmod 644 uploads/*

3. 监控系统资源

# 监控磁盘空间
df -h

# 监控内存使用
free -m

# 监控进程
ps aux | grep node

扩展功能

计划中的功能

  1. CDN集成: 支持阿里云OSS、腾讯云COS等
  2. 病毒扫描: 集成病毒扫描引擎
  3. 水印添加: 自动为图片添加水印
  4. 智能裁剪: AI驱动的智能图片裁剪
  5. 格式转换: 支持更多图片格式转换
  6. 批量处理: 支持批量图片处理
  7. 版本控制: 文件版本管理
  8. 权限控制: 细粒度的文件访问权限

集成建议

  1. 前端组件: 开发可复用的上传组件
  2. 移动端适配: 支持移动端文件上传
  3. 拖拽上传: 实现拖拽上传功能
  4. 进度显示: 显示上传进度和状态
  5. 预览功能: 上传前预览文件
  6. 批量操作: 支持批量选择和操作

总结

文件上传系统为解班客平台提供了完整、安全、高效的文件管理解决方案。通过模块化设计、完善的错误处理、详细的日志记录和性能优化,确保系统的稳定性和可维护性。

系统支持多种文件类型,提供了灵活的配置选项,能够满足不同场景的需求。同时,通过监控和统计功能,管理员可以实时了解系统状态,及时发现和解决问题。

未来将继续完善系统功能,增加更多高级特性,为用户提供更好的文件管理体验。