<template>
  <div class="help-content">
    <div class="content-header">
      <h2>消息文件存储与权限控制</h2>
      <el-button type="primary" size="small" plain @click="downloadDoc('/docs/minio_file_permissions_guide.md', 'MinIO_File_Permissions_Guide.md')">
        <el-icon style="margin-right: 5px"><Download /></el-icon>
        下载 开发文档
      </el-button>
    </div>
    <p class="intro">统一消息平台使用 MinIO 对象存储服务来管理聊天记录中的文件附件（图片、视频、文档等）。为了确保文件安全，系统采用双重权限控制机制：数据库层权限控制和存储层权限控制（预签名 URL）。</p>

    <div class="section">
      <h3>1. 概述</h3>
      <p>统一消息平台使用 MinIO 对象存储服务来管理聊天记录中的文件附件（图片、视频、文档等）。为了确保文件安全，系统采用<strong>双重权限控制机制</strong>：</p>
      <ul>
        <li><strong>数据库层权限控制</strong>：用户只能查询自己相关的消息</li>
        <li><strong>存储层权限控制</strong>：通过预签名 URL（Presigned URL）实现临时访问授权</li>
      </ul>

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

    <div class="section">
      <h3>2. 权限控制架构</h3>
      
      <h4>2.1 存储层设计</h4>
      <p>文件路径格式：<code>messages/{user_id}/{year}/{month}/{uuid}.{ext}</code></p>
      <p><strong>示例</strong>：<code>messages/123/2026/01/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg</code></p>

      <h4>2.2 权限控制流程</h4>
      <div class="code-block">
        <pre>
1. 用户上传文件 (需认证)
   ↓
2. 存储到私有Bucket
   ↓
3. 返回object_key
   ↓
4. 保存消息记录(content=object_key)
   ↓
5. 返回预签名URL(1小时有效)

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

    <div class="section">
      <h3>3. 开发者使用指南</h3>

      <h4>3.1 文件上传</h4>
      <p><strong>接口说明</strong>：</p>
      <ul>
        <li><strong>Endpoint</strong>: <code>POST /api/v1/messages/upload</code></li>
        <li><strong>认证方式</strong>: Bearer Token (用户认证) 或 应用签名认证</li>
        <li><strong>Content-Type</strong>: <code>multipart/form-data</code></li>
      </ul>

      <p><strong>请求示例</strong>：</p>
      <div class="code-block">
        <pre>
curl -X POST "{{API_BASE_URL}}/messages/upload" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -F "file=@/path/to/image.jpg"
        </pre>
      </div>

      <p><strong>响应示例</strong>：</p>
      <div class="code-block">
        <pre>
{
  "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
}
        </pre>
      </div>

      <p><strong>字段说明</strong>：</p>
      <table class="param-table">
        <thead>
          <tr><th>字段</th><th>类型</th><th>说明</th></tr>
        </thead>
        <tbody>
          <tr><td><code>url</code></td><td>string</td><td>预签名访问 URL（1小时有效）</td></tr>
          <tr><td><code>key</code></td><td>string</td><td>MinIO 对象键（object_key），用于存储到消息表</td></tr>
          <tr><td><code>filename</code></td><td>string</td><td>原始文件名</td></tr>
          <tr><td><code>content_type</code></td><td>string</td><td>MIME 类型</td></tr>
          <tr><td><code>size</code></td><td>int</td><td>文件大小（字节）</td></tr>
        </tbody>
      </table>

      <p><strong>前端示例 (JavaScript)</strong>：</p>
      <div class="code-block">
        <pre>
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;
}
        </pre>
      </div>

      <p><strong>文件类型限制</strong>：</p>
      <ul>
        <li><strong>图片</strong>: <code>image/jpeg</code>, <code>image/png</code>, <code>image/gif</code>, <code>image/webp</code></li>
        <li><strong>视频</strong>: <code>video/mp4</code>, <code>video/quicktime</code>, <code>video/x-msvideo</code></li>
        <li><strong>文档</strong>: <code>application/pdf</code>, <code>application/msword</code>, <code>application/vnd.openxmlformats-officedocument.*</code>, <code>text/plain</code></li>
      </ul>
      <p><strong>文件大小限制</strong>: 最大 50MB</p>

      <h4>3.2 发送带文件的消息</h4>
      <p>上传文件后，使用返回的 <code>key</code> 作为消息内容发送。</p>
      <div class="code-block">
        <pre>
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"
  }'
        </pre>
      </div>
      <p><strong>重要提示</strong>：</p>
      <ul>
        <li><code>content</code> 字段应存储 <code>key</code>（object_key），而不是完整的 URL</li>
        <li>系统会在查询消息时自动为文件类型生成预签名 URL</li>
      </ul>

      <h4>3.3 查询消息（自动生成文件访问链接）</h4>
      <p><strong>接口说明</strong>：</p>
      <ul>
        <li><strong>Endpoint</strong>: <code>GET /api/v1/messages/</code></li>
        <li><strong>认证方式</strong>: Bearer Token（仅用户认证）</li>
        <li><strong>权限</strong>: 用户只能查询自己作为接收者的消息</li>
      </ul>

      <p><strong>权限控制说明</strong>：</p>
      <ol>
        <li><strong>数据库层过滤</strong>：API 自动过滤，只返回 <code>receiver_id == current_user.id</code> 的消息</li>
        <li><strong>自动签名</strong>：对于 <code>content_type</code> 为 <code>IMAGE</code>、<code>VIDEO</code>、<code>FILE</code> 的消息，系统自动将 <code>content</code> 字段中的 <code>key</code> 转换为预签名 URL</li>
        <li><strong>URL 时效</strong>：预签名 URL 默认 1 小时有效</li>
      </ol>

      <p><strong>前端处理示例</strong>：</p>
      <div class="code-block">
        <pre>
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;
}
        </pre>
      </div>
    </div>

    <div class="section">
      <h3>4. 预签名 URL 机制详解</h3>

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

      <h4>4.2 生成时机</h4>
      <p>预签名 URL 在以下场景自动生成：</p>
      <ol>
        <li><strong>文件上传后</strong>：上传接口返回的 <code>url</code> 字段</li>
        <li><strong>查询消息时</strong>：<code>_process_message_content()</code> 函数自动处理</li>
        <li><strong>WebSocket 推送时</strong>：实时消息推送中的文件内容</li>
      </ol>

      <h4>4.3 URL 有效期</h4>
      <ul>
        <li><strong>默认有效期</strong>: 1 小时（3600 秒）</li>
        <li><strong>过期处理</strong>: URL 过期后，前端需要重新调用消息查询接口获取新的预签名 URL</li>
      </ul>
    </div>

    <div class="section">
      <h3>5. 安全最佳实践</h3>

      <h4>5.1 前端开发建议</h4>
      <ol>
        <li><strong>不要缓存预签名 URL</strong>
          <ul>
            <li>URL 有时效性，建议每次显示消息时重新获取</li>
            <li>如果 URL 过期，重新调用消息查询接口</li>
          </ul>
        </li>
        <li><strong>错误处理</strong>
          <div class="code-block">
            <pre>
async function loadImage(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      // URL 可能已过期，重新获取消息
      await refreshMessages();
    }
    return response.blob();
  } catch (error) {
    console.error('加载图片失败:', error);
  }
}
            </pre>
          </div>
        </li>
      </ol>

      <h4>5.2 后端开发建议</h4>
      <ol>
        <li><strong>不要直接暴露 object_key</strong>
          <ul>
            <li>消息表中的 <code>content</code> 字段存储的是 <code>key</code>，不是完整 URL</li>
            <li>只有在有权限查看消息时才生成预签名 URL</li>
          </ul>
        </li>
        <li><strong>权限验证</strong>
          <ul>
            <li>所有消息查询接口都已内置权限验证</li>
            <li>不要绕过权限检查直接生成预签名 URL</li>
          </ul>
        </li>
      </ol>
    </div>

    <div class="section">
      <h3>6. 常见问题 (FAQ)</h3>

      <h4>Q1: 为什么上传文件后返回的 URL 有时效性？</h4>
      <p><strong>A</strong>: 为了安全考虑，所有文件访问都通过预签名 URL。即使 URL 过期，用户仍可以通过查询消息接口重新获取有效的 URL。</p>

      <h4>Q2: 如何延长文件访问 URL 的有效期？</h4>
      <p><strong>A</strong>: 目前默认 1 小时有效期。如需调整，可以修改 <code>backend/app/core/minio.py</code> 中的 <code>get_presigned_url()</code> 方法的 <code>expires</code> 参数。</p>
      <div class="code-block">
        <pre>
# 修改为 24 小时
presigned_url = minio_storage.get_presigned_url(object_name, expires=timedelta(hours=24))
        </pre>
      </div>

      <h4>Q3: 用户能否直接访问 MinIO 中的文件？</h4>
      <p><strong>A</strong>: 不能。Bucket 设置为私有模式，所有文件必须通过预签名 URL 访问。即使知道文件路径，没有有效的预签名 URL 也无法访问。</p>

      <h4>Q4: 如何实现文件下载功能？</h4>
      <p><strong>A</strong>: 预签名 URL 支持直接下载。可以在前端添加下载按钮：</p>
      <div class="code-block">
        <pre>
function downloadFile(url, filename) {
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
}
        </pre>
      </div>

      <h4>Q5: 预签名 URL 过期后如何处理？</h4>
      <p><strong>A</strong>: 前端检测到 URL 过期（HTTP 403/404）时，重新调用消息查询接口获取新的预签名 URL。</p>

      <h4>Q6: 能否为其他用户生成文件访问链接？</h4>
      <p><strong>A</strong>: 不能。系统会验证消息权限，只有有权限查看消息的用户才能获取文件访问链接。这确保了文件访问的安全性。</p>

      <h4>Q7: 文件路径中的 user_id 是上传者还是接收者？</h4>
      <p><strong>A</strong>: 是上传者的 <code>user_id</code>。文件路径格式为 <code>messages/{uploader_user_id}/{year}/{month}/{uuid}.{ext}</code>。</p>
    </div>

    <div class="section">
      <h3>7. 技术实现细节</h3>

      <h4>7.1 核心代码位置</h4>
      <ul>
        <li><strong>MinIO 存储类</strong>: <code>backend/app/core/minio.py</code></li>
        <li><strong>文件上传接口</strong>: <code>backend/app/api/v1/endpoints/messages_upload.py</code></li>
        <li><strong>消息查询接口</strong>: <code>backend/app/api/v1/endpoints/messages.py</code></li>
        <li><strong>URL 生成逻辑</strong>: <code>_process_message_content()</code> 函数</li>
      </ul>

      <h4>7.2 配置说明</h4>
      <p>MinIO 相关配置在 <code>backend/app/core/config.py</code> 中：</p>
      <div class="code-block">
        <pre>
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
        </pre>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Download } from '@element-plus/icons-vue'
import { useHelpDocs } from '../../composables/useHelpDocs'

const { downloadDoc } = useHelpDocs()
</script>

<style scoped>
@import './help.css';
</style>
