Files
invest-mind-store/apps/api/资源上传文档.md
2026-01-16 18:01:41 +08:00

12 KiB
Raw Permalink Blame History

资源上传文档

概述

本系统提供了完整的文件上传和管理功能支持本地存储未来可扩展至云存储。主要用于上传券商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 文件中配置以下环境变量:

# 存储类型local本地存储未来可扩展为 qiniu、oss 等
STORAGE_TYPE=local

# 文件存储路径(相对于项目根目录)
STORAGE_PATH=./uploads

# 文件访问基础URL用于生成文件访问链接
STORAGE_BASE_URL=http://localhost:3200/uploads

配置说明

变量名 说明 默认值 示例
STORAGE_TYPE 存储类型 local localqiniu(未来支持)
STORAGE_PATH 文件存储路径 ./uploads ./uploads/var/www/uploads
STORAGE_BASE_URL 文件访问基础URL http://localhost:3200/uploads https://api.example.com/uploads

生产环境配置建议

# 生产环境配置示例
STORAGE_TYPE=local
STORAGE_PATH=/var/www/invest-mind/uploads
STORAGE_BASE_URL=https://api.example.com/uploads

静态文件服务配置

系统在 main.ts 中自动配置了静态文件服务,无需额外配置:

// 配置静态文件服务
const storagePath = configService.get<string>('STORAGE_PATH') || './uploads';
app.useStaticAssets(join(process.cwd(), storagePath), {
    prefix: '/uploads/',
});

这意味着所有存储在 STORAGE_PATH 目录下的文件都可以通过 /uploads/ 前缀访问。

API 接口

1. 管理员上传文件

接口地址: POST /api/storage/upload

权限要求: 需要管理员权限(adminsuper_admin

请求格式: multipart/form-data

请求参数:

参数名 类型 必填 说明 可选值
file File 要上传的文件 -
folder string 存储文件夹 brokerusertemp
filename string 自定义文件名 -

文件限制:

  • 最大文件大小5MB
  • 允许的文件类型:image/jpegimage/jpgimage/pngimage/gifimage/webp

请求示例:

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"

响应示例:

{
    "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/jpegimage/jpgimage/pngimage/gifimage/webp
  • 文件固定存储在 user 文件夹

请求示例:

curl -X POST http://localhost:3200/api/storage/upload/avatar \
  -F "file=@/path/to/avatar.jpg"

响应示例:

{
    "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}

权限要求: 需要管理员权限(adminsuper_admin

路径参数:

参数名 类型 说明 示例
path string 文件相对路径 broker/1234567890-abcdef-broker-logo.jpg

请求示例:

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:时间戳(毫秒)
  • random8字节随机十六进制字符串
  • originalname:原始文件名(去除特殊字符)

文件名清理规则

系统会自动清理文件名中的特殊字符:

  • 保留:字母、数字、下划线 _、连字符 -
  • 替换:其他特殊字符会被替换为下划线 _

文件存储结构

本地存储的文件结构如下:

uploads/
├── broker/          # 券商相关文件
│   └── 1234567890-abcdef-broker-logo.jpg
├── user/            # 用户相关文件
│   └── 1234567890-abcdef-avatar.jpg
└── temp/            # 临时文件
    └── 1234567890-abcdef-temp-file.jpg

安全特性

1. 路径遍历防护

删除文件时会进行安全检查,防止路径遍历攻击:

// 安全检查:确保文件路径在 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 示例

// 上传文件
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 示例

import { useState } from 'react';

function FileUpload() {
    const [file, setFile] = useState<File | null>(null);
    const [uploading, setUploading] = useState(false);
    const [result, setResult] = useState<any>(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 (
        <div>
            <input
                type="file"
                accept="image/*"
                onChange={(e) => setFile(e.target.files?.[0] || null)}
            />
            <button onClick={handleUpload} disabled={uploading}>
                {uploading ? '上传中...' : '上传'}
            </button>
            {result && (
                <div>
                    <p>上传成功!</p>
                    <img src={result.url} alt="上传的文件" />
                </div>
            )}
        </div>
    );
}

扩展性

添加新的存储提供者

系统采用策略模式可以轻松添加新的存储提供者如七牛云、阿里云OSS等

  1. 实现 IStorageProvider 接口
  2. StorageService 中添加新的存储类型判断
  3. 配置相应的环境变量

示例:

// 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 - 静态文件服务配置

更新日志

  • 初始版本:支持本地存储、文件上传、文件删除功能