feat: 更新持仓
This commit is contained in:
439
apps/api/资源上传文档.md
Normal file
439
apps/api/资源上传文档.md
Normal file
@@ -0,0 +1,439 @@
|
||||
# 资源上传文档
|
||||
|
||||
## 概述
|
||||
|
||||
本系统提供了完整的文件上传和管理功能,支持本地存储(未来可扩展至云存储)。主要用于上传券商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<string>('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<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. 配置相应的环境变量
|
||||
|
||||
示例:
|
||||
|
||||
```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` - 静态文件服务配置
|
||||
|
||||
## 更新日志
|
||||
|
||||
- 初始版本:支持本地存储、文件上传、文件删除功能
|
||||
Reference in New Issue
Block a user