# 资源上传文档 ## 概述 本系统提供了完整的文件上传和管理功能,支持本地存储(未来可扩展至云存储)。主要用于上传券商Logo、用户头像等资源文件。 ## 目录结构 ``` storage/ ├── dto/ │ └── upload-file.dto.ts # 上传文件DTO ├── interfaces/ │ └── storage-provider.interface.ts # 存储提供者接口 ├── providers/ │ └── local-storage.provider.ts # 本地存储实现 ├── storage.controller.ts # 存储控制器 ├── storage.module.ts # 存储模块 └── storage.service.ts # 存储服务 ``` ## 环境配置 ### 必需的环境变量 在 `.env` 文件中配置以下环境变量: ```env # 存储类型:local(本地存储),未来可扩展为 qiniu、oss 等 STORAGE_TYPE=local # 文件存储路径(相对于项目根目录) STORAGE_PATH=./uploads # 文件访问基础URL(用于生成文件访问链接) STORAGE_BASE_URL=http://localhost:3200/uploads ``` ### 配置说明 | 变量名 | 说明 | 默认值 | 示例 | | ------------------ | --------------- | ------------------------------- | --------------------------------- | | `STORAGE_TYPE` | 存储类型 | `local` | `local`、`qiniu`(未来支持) | | `STORAGE_PATH` | 文件存储路径 | `./uploads` | `./uploads`、`/var/www/uploads` | | `STORAGE_BASE_URL` | 文件访问基础URL | `http://localhost:3200/uploads` | `https://api.example.com/uploads` | ### 生产环境配置建议 ```env # 生产环境配置示例 STORAGE_TYPE=local STORAGE_PATH=/var/www/invest-mind/uploads STORAGE_BASE_URL=https://api.example.com/uploads ``` ## 静态文件服务配置 系统在 `main.ts` 中自动配置了静态文件服务,无需额外配置: ```typescript // 配置静态文件服务 const storagePath = configService.get('STORAGE_PATH') || './uploads'; app.useStaticAssets(join(process.cwd(), storagePath), { prefix: '/uploads/', }); ``` 这意味着所有存储在 `STORAGE_PATH` 目录下的文件都可以通过 `/uploads/` 前缀访问。 ## API 接口 ### 1. 管理员上传文件 **接口地址:** `POST /api/storage/upload` **权限要求:** 需要管理员权限(`admin` 或 `super_admin`) **请求格式:** `multipart/form-data` **请求参数:** | 参数名 | 类型 | 必填 | 说明 | 可选值 | | ---------- | ------ | ---- | ------------ | ------------------------ | | `file` | File | 是 | 要上传的文件 | - | | `folder` | string | 否 | 存储文件夹 | `broker`、`user`、`temp` | | `filename` | string | 否 | 自定义文件名 | - | **文件限制:** - 最大文件大小:5MB - 允许的文件类型:`image/jpeg`、`image/jpg`、`image/png`、`image/gif`、`image/webp` **请求示例:** ```bash curl -X POST http://localhost:3200/api/storage/upload \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -F "file=@/path/to/image.jpg" \ -F "folder=broker" \ -F "filename=custom-name.jpg" ``` **响应示例:** ```json { "path": "broker/1234567890-abcdef-broker-logo.jpg", "url": "http://localhost:3200/uploads/broker/1234567890-abcdef-broker-logo.jpg", "filename": "1234567890-abcdef-broker-logo.jpg", "size": 102400, "mimetype": "image/jpeg" } ``` ### 2. 用户上传头像 **接口地址:** `POST /api/storage/upload/avatar` **权限要求:** 无需鉴权(公开接口) **请求格式:** `multipart/form-data` **请求参数:** | 参数名 | 类型 | 必填 | 说明 | | ------ | ---- | ---- | ---------------- | | `file` | File | 是 | 要上传的头像文件 | **文件限制:** - 最大文件大小:2MB - 允许的文件类型:`image/jpeg`、`image/jpg`、`image/png`、`image/gif`、`image/webp` - 文件固定存储在 `user` 文件夹 **请求示例:** ```bash curl -X POST http://localhost:3200/api/storage/upload/avatar \ -F "file=@/path/to/avatar.jpg" ``` **响应示例:** ```json { "path": "user/1234567890-abcdef-avatar.jpg", "url": "http://localhost:3200/uploads/user/1234567890-abcdef-avatar.jpg", "filename": "1234567890-abcdef-avatar.jpg", "size": 51200, "mimetype": "image/jpeg" } ``` ### 3. 删除文件 **接口地址:** `DELETE /api/storage/{path}` **权限要求:** 需要管理员权限(`admin` 或 `super_admin`) **路径参数:** | 参数名 | 类型 | 说明 | 示例 | | ------ | ------ | ------------ | ------------------------------------------ | | `path` | string | 文件相对路径 | `broker/1234567890-abcdef-broker-logo.jpg` | **请求示例:** ```bash curl -X DELETE http://localhost:3200/api/storage/broker/1234567890-abcdef-broker-logo.jpg \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` **响应:** - 成功:`204 No Content` - 文件不存在:`404 Not Found` - 权限不足:`403 Forbidden` ## 文件夹类型说明 系统支持三种文件夹类型,用于分类存储不同类型的文件: | 文件夹 | 用途 | 说明 | | -------- | ------------ | ------------------------------ | | `broker` | 券商相关文件 | 用于存储券商Logo等基础数据文件 | | `user` | 用户相关文件 | 用于存储用户头像等个人文件 | | `temp` | 临时文件 | 用于存储临时文件,可定期清理 | ## 文件访问 ### 访问方式 上传成功后,系统会返回文件的访问URL,可以通过以下方式访问: 1. **直接访问URL**:使用返回的 `url` 字段直接访问 ``` http://localhost:3200/uploads/broker/1234567890-abcdef-broker-logo.jpg ``` 2. **通过静态文件服务**:所有文件都通过 `/uploads/` 前缀提供静态文件服务 ``` http://localhost:3200/uploads/{folder}/{filename} ``` ### 文件命名规则 如果不指定自定义文件名,系统会自动生成文件名,格式为: ``` {timestamp}-{random}-{originalname} ``` 例如: - 原始文件名:`logo.jpg` - 生成文件名:`1234567890-abcdef-logo.jpg` 其中: - `timestamp`:时间戳(毫秒) - `random`:8字节随机十六进制字符串 - `originalname`:原始文件名(去除特殊字符) ### 文件名清理规则 系统会自动清理文件名中的特殊字符: - 保留:字母、数字、下划线 `_`、连字符 `-` - 替换:其他特殊字符会被替换为下划线 `_` ## 文件存储结构 本地存储的文件结构如下: ``` uploads/ ├── broker/ # 券商相关文件 │ └── 1234567890-abcdef-broker-logo.jpg ├── user/ # 用户相关文件 │ └── 1234567890-abcdef-avatar.jpg └── temp/ # 临时文件 └── 1234567890-abcdef-temp-file.jpg ``` ## 安全特性 ### 1. 路径遍历防护 删除文件时会进行安全检查,防止路径遍历攻击: ```typescript // 安全检查:确保文件路径在 basePath 内 const resolvedPath = path.resolve(fullPath); const resolvedBasePath = path.resolve(this.basePath); if (!resolvedPath.startsWith(resolvedBasePath)) { throw new Error('非法文件路径'); } ``` ### 2. 文件类型验证 - 仅允许上传图片文件(jpeg、jpg、png、gif、webp) - 通过 MIME 类型和文件扩展名双重验证 ### 3. 文件大小限制 - 管理员上传:最大 5MB - 用户头像:最大 2MB ### 4. 权限控制 - 管理员上传和删除操作需要管理员权限 - 用户头像上传无需鉴权(用于注册场景) ## 使用示例 ### JavaScript/TypeScript 示例 ```typescript // 上传文件 const formData = new FormData(); formData.append('file', fileInput.files[0]); formData.append('folder', 'broker'); formData.append('filename', 'custom-name.jpg'); const response = await fetch('http://localhost:3200/api/storage/upload', { method: 'POST', headers: { Authorization: `Bearer ${token}`, }, body: formData, }); const result = await response.json(); console.log('文件URL:', result.url); ``` ### React 示例 ```tsx import { useState } from 'react'; function FileUpload() { const [file, setFile] = useState(null); const [uploading, setUploading] = useState(false); const [result, setResult] = useState(null); const handleUpload = async () => { if (!file) return; setUploading(true); const formData = new FormData(); formData.append('file', file); formData.append('folder', 'broker'); try { const response = await fetch( 'http://localhost:3200/api/storage/upload', { method: 'POST', headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, }, body: formData, }, ); const data = await response.json(); setResult(data); } catch (error) { console.error('上传失败:', error); } finally { setUploading(false); } }; return (
setFile(e.target.files?.[0] || null)} /> {result && (

上传成功!

上传的文件
)}
); } ``` ## 扩展性 ### 添加新的存储提供者 系统采用策略模式,可以轻松添加新的存储提供者(如七牛云、阿里云OSS等): 1. 实现 `IStorageProvider` 接口 2. 在 `StorageService` 中添加新的存储类型判断 3. 配置相应的环境变量 示例: ```typescript // providers/qiniu-storage.provider.ts export class QiniuStorageProvider implements IStorageProvider { // 实现接口方法 } // storage.service.ts switch (storageType) { case 'qiniu': this.provider = new QiniuStorageProvider(configService); break; } ``` ## 常见问题 ### 1. 文件上传失败 **问题:** 上传文件时返回 400 错误 **解决方案:** - 检查文件大小是否超过限制(管理员5MB,头像2MB) - 检查文件类型是否为支持的图片格式 - 检查文件是否损坏 ### 2. 文件无法访问 **问题:** 上传成功但无法通过URL访问文件 **解决方案:** - 检查 `STORAGE_BASE_URL` 配置是否正确 - 检查静态文件服务是否正常启动 - 检查文件是否实际存在于 `STORAGE_PATH` 目录 - 检查文件权限是否正确 ### 3. 删除文件失败 **问题:** 删除文件时返回 404 或 403 错误 **解决方案:** - 404:检查文件路径是否正确,文件是否存在 - 403:检查是否有管理员权限 - 检查文件路径是否包含非法字符 ### 4. 生产环境配置 **问题:** 如何在生产环境配置文件存储 **解决方案:** 1. 设置 `STORAGE_PATH` 为绝对路径(如 `/var/www/uploads`) 2. 设置 `STORAGE_BASE_URL` 为生产域名(如 `https://api.example.com/uploads`) 3. 确保目录有写入权限:`chmod -R 755 /var/www/uploads` 4. 考虑使用云存储服务(未来支持) ## 相关文件 - `storage.controller.ts` - API 控制器 - `storage.service.ts` - 存储服务 - `local-storage.provider.ts` - 本地存储实现 - `storage-provider.interface.ts` - 存储提供者接口 - `main.ts` - 静态文件服务配置 ## 更新日志 - 初始版本:支持本地存储、文件上传、文件删除功能