# 消息文件存储与权限控制开发者指南

**版本**: V1.0  
**日期**: 2026-01-XX  
**适用场景**: 聊天记录中的图片、视频、文件等附件存储与访问

---

## 1. 概述

统一消息平台使用 MinIO 对象存储服务来管理聊天记录中的文件附件（图片、视频、文档等）。为了确保文件安全，系统采用**双重权限控制机制**：

1. **数据库层权限控制**：用户只能查询自己相关的消息
2. **存储层权限控制**：通过预签名 URL（Presigned URL）实现临时访问授权

### 核心特性

- ✅ **私有存储**：所有文件存储在私有 Bucket 中，无法直接访问
- ✅ **权限隔离**：只有有权限查看消息的用户才能获取文件访问链接
- ✅ **时效控制**：预签名 URL 默认 1 小时有效，过期后需重新获取
- ✅ **路径隔离**：文件路径包含用户 ID，便于后续扩展细粒度权限

---

## 2. 权限控制架构

### 2.1 存储层设计

```
MinIO Bucket: unified-message-files
├── messages/
│   ├── {user_id}/
│   │   ├── {year}/
│   │   │   ├── {month}/
│   │   │   │   ├── {uuid}.{ext}
```

**文件路径格式**: `messages/{user_id}/{year}/{month}/{uuid}.{ext}`

**示例**: `messages/123/2026/01/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg`

### 2.2 权限控制流程

```
1. 用户上传文件 (需认证)
   ↓
2. 存储到私有Bucket
   ↓
3. 返回object_key
   ↓
4. 保存消息记录(content=object_key)
   ↓
5. 返回预签名URL(1小时有效)

6. 用户查询消息列表 (需认证)
   ↓
7. 权限过滤(仅查询自己的消息)
   ↓
8. 返回消息列表
   ↓
9. 为文件类型生成预签名URL
   ↓
10. 返回消息(含签名URL)
```

---

## 3. 开发者使用指南

### 3.1 文件上传

#### 接口说明

- **Endpoint**: `POST /api/v1/messages/upload`
- **认证方式**: Bearer Token (用户认证) 或 应用签名认证
- **Content-Type**: `multipart/form-data`

#### 请求示例

```bash
curl -X POST "{{API_BASE_URL}}/messages/upload" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -F "file=@/path/to/image.jpg"
```

#### 响应示例

```json
{
  "url": "https://minio.example.com/bucket/messages/123/2026/01/uuid.jpg?X-Amz-Algorithm=...&X-Amz-Expires=3600",
  "key": "messages/123/2026/01/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg",
  "filename": "image.jpg",
  "content_type": "image/jpeg",
  "size": 102400
}
```

#### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `url` | string | 预签名访问 URL（1小时有效） |
| `key` | string | MinIO 对象键（object_key），用于存储到消息表 |
| `filename` | string | 原始文件名 |
| `content_type` | string | MIME 类型 |
| `size` | int | 文件大小（字节） |

#### 前端示例 (JavaScript)

```javascript
async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);

  const response = await fetch('/api/v1/messages/upload', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`
    },
    body: formData
  });

  const result = await response.json();
  
  // 使用 result.key 发送消息
  return result;
}
```

#### 文件类型限制

系统支持以下文件类型：

- **图片**: `image/jpeg`, `image/png`, `image/gif`, `image/webp`
- **视频**: `video/mp4`, `video/quicktime`, `video/x-msvideo`
- **文档**: `application/pdf`, `application/msword`, `application/vnd.openxmlformats-officedocument.*`, `text/plain`

**文件大小限制**: 最大 50MB

---

### 3.2 发送带文件的消息

上传文件后，使用返回的 `key` 作为消息内容发送。

#### 请求示例

```bash
curl -X POST "{{API_BASE_URL}}/messages/" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "receiver_id": 456,
    "type": "MESSAGE",
    "content_type": "IMAGE",
    "title": "分享图片",
    "content": "messages/123/2026/01/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg"
  }'
```

**重要提示**：
- `content` 字段应存储 `key`（object_key），而不是完整的 URL
- 系统会在查询消息时自动为文件类型生成预签名 URL

---

### 3.3 查询消息（自动生成文件访问链接）

#### 接口说明

- **Endpoint**: `GET /api/v1/messages/`
- **认证方式**: Bearer Token（仅用户认证）
- **权限**: 用户只能查询自己作为接收者的消息

#### 请求示例

```bash
curl -X GET "{{API_BASE_URL}}/messages/?skip=0&limit=20" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"
```

#### 响应示例

```json
[
  {
    "id": 1001,
    "sender_id": 123,
    "receiver_id": 456,
    "type": "MESSAGE",
    "content_type": "IMAGE",
    "title": "分享图片",
    "content": "https://minio.example.com/bucket/messages/123/2026/01/uuid.jpg?X-Amz-Algorithm=...&X-Amz-Expires=3600",
    "is_read": false,
    "created_at": "2026-01-15T10:30:00"
  }
]
```

#### 权限控制说明

1. **数据库层过滤**：API 自动过滤，只返回 `receiver_id == current_user.id` 的消息
2. **自动签名**：对于 `content_type` 为 `IMAGE`、`VIDEO`、`FILE` 的消息，系统自动将 `content` 字段中的 `key` 转换为预签名 URL
3. **URL 时效**：预签名 URL 默认 1 小时有效

#### 前端处理示例

```javascript
async function fetchMessages() {
  const response = await fetch('/api/v1/messages/', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  
  const messages = await response.json();
  
  messages.forEach(msg => {
    if (['IMAGE', 'VIDEO', 'FILE'].includes(msg.content_type)) {
      // content 已经是预签名 URL，直接使用
      console.log('文件URL:', msg.content);
    }
  });
  
  return messages;
}
```

---

### 3.4 聊天历史记录

#### 接口说明

- **Endpoint**: `GET /api/v1/messages/history/{other_user_id}`
- **认证方式**: Bearer Token
- **权限**: 只能查询与当前用户的聊天记录

#### 请求示例

```bash
curl -X GET "{{API_BASE_URL}}/messages/history/456?skip=0&limit=50" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"
```

#### 权限验证逻辑

系统会验证：
- 消息的 `sender_id` 或 `receiver_id` 必须包含当前用户
- 只有通过验证的消息才会返回，并自动生成文件预签名 URL

---

## 4. 预签名 URL 机制详解

### 4.1 什么是预签名 URL？

预签名 URL（Presigned URL）是 MinIO/S3 提供的一种临时访问授权机制。它允许在不需要公开 Bucket 的情况下，为特定对象生成一个有时效性的访问链接。

### 4.2 生成时机

预签名 URL 在以下场景自动生成：

1. **文件上传后**：上传接口返回的 `url` 字段
2. **查询消息时**：`_process_message_content()` 函数自动处理
3. **WebSocket 推送时**：实时消息推送中的文件内容

### 4.3 URL 有效期

- **默认有效期**: 1 小时（3600 秒）
- **过期处理**: URL 过期后，前端需要重新调用消息查询接口获取新的预签名 URL

### 4.4 URL 格式示例

```
https://minio.example.com:9004/unified-message-files/messages/123/2026/01/uuid.jpg
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=ACCESS_KEY%2F20260115%2Fus-east-1%2Fs3%2Faws4_request
&X-Amz-Date=20260115T103000Z
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=abc123...
```

---

## 5. 安全最佳实践

### 5.1 前端开发建议

1. **不要缓存预签名 URL**
   - URL 有时效性，建议每次显示消息时重新获取
   - 如果 URL 过期，重新调用消息查询接口

2. **错误处理**
   ```javascript
   async function loadImage(url) {
     try {
       const response = await fetch(url);
       if (!response.ok) {
         // URL 可能已过期，重新获取消息
         await refreshMessages();
       }
       return response.blob();
     } catch (error) {
       console.error('加载图片失败:', error);
     }
   }
   ```

3. **图片预览优化**
   - 对于大文件，考虑在前端添加加载状态
   - 可以添加图片压缩或缩略图功能

### 5.2 后端开发建议

1. **不要直接暴露 object_key**
   - 消息表中的 `content` 字段存储的是 `key`，不是完整 URL
   - 只有在有权限查看消息时才生成预签名 URL

2. **权限验证**
   - 所有消息查询接口都已内置权限验证
   - 不要绕过权限检查直接生成预签名 URL

3. **文件清理**
   - 考虑实现定期清理机制，删除长时间未访问的文件
   - 可以在删除消息时同步删除 MinIO 中的文件

---

## 6. 常见问题 (FAQ)

### Q1: 为什么上传文件后返回的 URL 有时效性？

**A**: 为了安全考虑，所有文件访问都通过预签名 URL。即使 URL 过期，用户仍可以通过查询消息接口重新获取有效的 URL。

### Q2: 如何延长文件访问 URL 的有效期？

**A**: 目前默认 1 小时有效期。如需调整，可以修改 `backend/app/core/minio.py` 中的 `get_presigned_url()` 方法的 `expires` 参数。

```python
# 修改为 24 小时
presigned_url = minio_storage.get_presigned_url(object_name, expires=timedelta(hours=24))
```

### Q3: 用户能否直接访问 MinIO 中的文件？

**A**: 不能。Bucket 设置为私有模式，所有文件必须通过预签名 URL 访问。即使知道文件路径，没有有效的预签名 URL 也无法访问。

### Q4: 如何实现文件下载功能？

**A**: 预签名 URL 支持直接下载。可以在前端添加下载按钮：

```javascript
function downloadFile(url, filename) {
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
}
```

### Q5: 预签名 URL 过期后如何处理？

**A**: 前端检测到 URL 过期（HTTP 403/404）时，重新调用消息查询接口获取新的预签名 URL。

### Q6: 能否为其他用户生成文件访问链接？

**A**: 不能。系统会验证消息权限，只有有权限查看消息的用户才能获取文件访问链接。这确保了文件访问的安全性。

### Q7: 文件路径中的 user_id 是上传者还是接收者？

**A**: 是上传者的 `user_id`。文件路径格式为 `messages/{uploader_user_id}/{year}/{month}/{uuid}.{ext}`。

---

## 7. 技术实现细节

### 7.1 核心代码位置

- **MinIO 存储类**: `backend/app/core/minio.py`
- **文件上传接口**: `backend/app/api/v1/endpoints/messages_upload.py`
- **消息查询接口**: `backend/app/api/v1/endpoints/messages.py`
- **URL 生成逻辑**: `_process_message_content()` 函数

### 7.2 配置说明

MinIO 相关配置在 `backend/app/core/config.py` 中：

```python
MINIO_ENDPOINT: str = "https://api.hnyunzhu.com:9004"
MINIO_ACCESS_KEY: str = "your_access_key"
MINIO_SECRET_KEY: str = "your_secret_key"
MINIO_BUCKET_NAME: str = "unified-message-files"
MINIO_SECURE: bool = False
```

---

## 8. 更新日志

### V1.0 (2026-01-XX)
- 初始版本
- 实现基于预签名 URL 的文件权限控制
- 支持图片、视频、文档等文件类型
- 默认 1 小时 URL 有效期

---

## 9. 技术支持

如有问题或建议，请联系开发团队或提交 Issue。

**相关文档**:
- [消息系统 API 文档](./api_message_system_v1.md)
- [消息集成指南](./message_integration.md)
