- 创建 docs/ 目录存放所有文档 - QUICK_START.md: 快速入门指南 - OPERATIONS_GUIDE.md: 完整运维指南 - VUE_API_INTEGRATION.md: Vue 集成文档 - DEPLOYMENT_INFO.md: 部署配置信息 - versions.md: 版本信息 - 创建 examples/ 目录存放示例代码 - storage_client.py: Python 完整客户端 - storage_client.js: JavaScript 完整客户端 - test_https_storage.py: 功能测试脚本 - 新增 README_STORAGE.md 作为 Storage 使用指南 - 修复签名 URL 生成问题(需要 /storage/v1 前缀) - 测试脚本支持资源已存在的情况 - 所有客户端代码已验证可用 功能特性: ✓ 公网 HTTPS 访问 ✓ 文件上传/下载 ✓ 生成临时下载链接 ✓ 完整的 REST API 客户端 ✓ 支持 Python 和 JavaScript
1239 lines
27 KiB
Markdown
1239 lines
27 KiB
Markdown
# Supabase API 集成文档 - Vue 前端对接指南
|
||
|
||
## 📋 目录
|
||
|
||
- [基本信息](#基本信息)
|
||
- [快速开始](#快速开始)
|
||
- [API 端点](#api-端点)
|
||
- [认证配置](#认证配置)
|
||
- [REST API 使用](#rest-api-使用)
|
||
- [Auth API 使用](#auth-api-使用)
|
||
- [Storage API 使用](#storage-api-使用)
|
||
- [Realtime 使用](#realtime-使用)
|
||
- [完整示例](#完整示例)
|
||
- [错误处理](#错误处理)
|
||
|
||
---
|
||
|
||
## 基本信息
|
||
|
||
### 🌐 服务地址
|
||
|
||
```
|
||
Base URL: https://amiap.hzau.edu.cn/supa
|
||
内网地址: http://100.64.0.2:18000
|
||
Dashboard: http://100.64.0.2:18000 (内网访问)
|
||
```
|
||
|
||
### 🔑 认证密钥
|
||
|
||
```javascript
|
||
// 从 .env 文件获取
|
||
const SUPABASE_URL = 'https://amiap.hzau.edu.cn/supa'
|
||
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg'
|
||
|
||
// ⚠️ 仅后端使用!不要暴露给前端
|
||
const SUPABASE_SERVICE_ROLE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3NjM4MDI2NjksImV4cCI6MjA3OTE2MjY2OX0.gQWUaTkZ6mjjlv2TED0cODp2meqqWuCGKZR1ptIbovg'
|
||
```
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 1. 安装 Supabase 客户端
|
||
|
||
```bash
|
||
npm install @supabase/supabase-js
|
||
```
|
||
|
||
### 2. 创建 Supabase 客户端
|
||
|
||
```javascript
|
||
// src/lib/supabaseClient.js
|
||
import { createClient } from '@supabase/supabase-js'
|
||
|
||
const supabaseUrl = 'https://amiap.hzau.edu.cn/supa'
|
||
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg'
|
||
|
||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||
```
|
||
|
||
### 3. 在 Vue 组件中使用
|
||
|
||
```vue
|
||
<script setup>
|
||
import { supabase } from '@/lib/supabaseClient'
|
||
import { ref, onMounted } from 'vue'
|
||
|
||
const users = ref([])
|
||
|
||
onMounted(async () => {
|
||
const { data, error } = await supabase
|
||
.from('users')
|
||
.select('*')
|
||
|
||
if (error) {
|
||
console.error('Error:', error)
|
||
} else {
|
||
users.value = data
|
||
}
|
||
})
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## API 端点
|
||
|
||
### ✅ 可用的 API 端点
|
||
|
||
| 服务 | 端点 | 用途 | 状态 |
|
||
|------|------|------|------|
|
||
| REST API | `https://amiap.hzau.edu.cn/supa/rest/v1/` | 数据库操作 | ✅ 可用 |
|
||
| Auth API | `https://amiap.hzau.edu.cn/supa/auth/v1/` | 用户认证 | ✅ 可用 |
|
||
| Storage API | `https://amiap.hzau.edu.cn/supa/storage/v1/` | 文件存储 | ✅ 可用 |
|
||
| Realtime | `wss://amiap.hzau.edu.cn/supa/realtime/v1/` | 实时订阅 | ✅ 可用 |
|
||
| Edge Functions | `https://amiap.hzau.edu.cn/supa/functions/v1/` | 云函数 | ⚠️ 需配置 |
|
||
|
||
---
|
||
|
||
## 认证配置
|
||
|
||
### 环境变量配置
|
||
|
||
创建 `.env` 文件:
|
||
|
||
```env
|
||
VITE_SUPABASE_URL=https://amiap.hzau.edu.cn/supa
|
||
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg
|
||
```
|
||
|
||
### 使用环境变量
|
||
|
||
```javascript
|
||
// src/lib/supabaseClient.js
|
||
import { createClient } from '@supabase/supabase-js'
|
||
|
||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||
|
||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||
```
|
||
|
||
---
|
||
|
||
## REST API 使用
|
||
|
||
### 查询数据
|
||
|
||
```javascript
|
||
// 查询所有记录
|
||
const { data, error } = await supabase
|
||
.from('users')
|
||
.select('*')
|
||
|
||
// 条件查询
|
||
const { data, error } = await supabase
|
||
.from('users')
|
||
.select('id, name, email')
|
||
.eq('status', 'active')
|
||
.order('created_at', { ascending: false })
|
||
.limit(10)
|
||
|
||
// 关联查询
|
||
const { data, error } = await supabase
|
||
.from('posts')
|
||
.select(`
|
||
*,
|
||
author:users(name, email),
|
||
comments(*)
|
||
`)
|
||
```
|
||
|
||
### 插入数据
|
||
|
||
```javascript
|
||
const { data, error } = await supabase
|
||
.from('users')
|
||
.insert([
|
||
{ name: 'John Doe', email: 'john@example.com' }
|
||
])
|
||
.select()
|
||
```
|
||
|
||
### 更新数据
|
||
|
||
```javascript
|
||
const { data, error } = await supabase
|
||
.from('users')
|
||
.update({ status: 'inactive' })
|
||
.eq('id', userId)
|
||
.select()
|
||
```
|
||
|
||
### 删除数据
|
||
|
||
```javascript
|
||
const { data, error } = await supabase
|
||
.from('users')
|
||
.delete()
|
||
.eq('id', userId)
|
||
```
|
||
|
||
---
|
||
|
||
## Auth API 使用
|
||
|
||
### 用户注册
|
||
|
||
```javascript
|
||
const { data, error } = await supabase.auth.signUp({
|
||
email: 'user@example.com',
|
||
password: 'password123',
|
||
options: {
|
||
data: {
|
||
first_name: 'John',
|
||
last_name: 'Doe'
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
### 用户登录
|
||
|
||
```javascript
|
||
// 邮箱密码登录
|
||
const { data, error } = await supabase.auth.signInWithPassword({
|
||
email: 'user@example.com',
|
||
password: 'password123'
|
||
})
|
||
|
||
// OAuth 登录(需要配置)
|
||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||
provider: 'google'
|
||
})
|
||
```
|
||
|
||
### 获取当前用户
|
||
|
||
```javascript
|
||
const { data: { user } } = await supabase.auth.getUser()
|
||
```
|
||
|
||
### 退出登录
|
||
|
||
```javascript
|
||
const { error } = await supabase.auth.signOut()
|
||
```
|
||
|
||
### 监听认证状态
|
||
|
||
```javascript
|
||
supabase.auth.onAuthStateChange((event, session) => {
|
||
console.log(event, session)
|
||
|
||
if (event === 'SIGNED_IN') {
|
||
console.log('用户已登录:', session.user)
|
||
}
|
||
if (event === 'SIGNED_OUT') {
|
||
console.log('用户已退出')
|
||
}
|
||
})
|
||
```
|
||
|
||
### Vue 3 Composition API 示例
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import { supabase } from '@/lib/supabaseClient'
|
||
import { useRouter } from 'vue-router'
|
||
|
||
const router = useRouter()
|
||
const email = ref('')
|
||
const password = ref('')
|
||
const user = ref(null)
|
||
|
||
// 登录
|
||
const signIn = async () => {
|
||
const { data, error } = await supabase.auth.signInWithPassword({
|
||
email: email.value,
|
||
password: password.value
|
||
})
|
||
|
||
if (error) {
|
||
alert('登录失败: ' + error.message)
|
||
} else {
|
||
user.value = data.user
|
||
router.push('/dashboard')
|
||
}
|
||
}
|
||
|
||
// 退出
|
||
const signOut = async () => {
|
||
await supabase.auth.signOut()
|
||
user.value = null
|
||
router.push('/login')
|
||
}
|
||
|
||
// 检查当前用户
|
||
onMounted(async () => {
|
||
const { data: { user: currentUser } } = await supabase.auth.getUser()
|
||
user.value = currentUser
|
||
|
||
// 监听认证状态变化
|
||
supabase.auth.onAuthStateChange((event, session) => {
|
||
user.value = session?.user || null
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="!user">
|
||
<input v-model="email" type="email" placeholder="邮箱" />
|
||
<input v-model="password" type="password" placeholder="密码" />
|
||
<button @click="signIn">登录</button>
|
||
</div>
|
||
<div v-else>
|
||
<p>欢迎, {{ user.email }}</p>
|
||
<button @click="signOut">退出</button>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
## Storage API 使用
|
||
|
||
### 上传文件
|
||
|
||
```javascript
|
||
const file = event.target.files[0]
|
||
const fileName = `${Date.now()}-${file.name}`
|
||
|
||
const { data, error } = await supabase.storage
|
||
.from('avatars')
|
||
.upload(fileName, file)
|
||
|
||
if (error) {
|
||
console.error('上传失败:', error)
|
||
} else {
|
||
console.log('上传成功:', data)
|
||
}
|
||
```
|
||
|
||
### 下载文件
|
||
|
||
```javascript
|
||
const { data, error } = await supabase.storage
|
||
.from('avatars')
|
||
.download('path/to/file.jpg')
|
||
```
|
||
|
||
### 获取公开 URL
|
||
|
||
```javascript
|
||
const { data } = supabase.storage
|
||
.from('avatars')
|
||
.getPublicUrl('path/to/file.jpg')
|
||
|
||
console.log('公开URL:', data.publicUrl)
|
||
```
|
||
|
||
### 删除文件
|
||
|
||
```javascript
|
||
const { data, error } = await supabase.storage
|
||
.from('avatars')
|
||
.remove(['path/to/file.jpg'])
|
||
```
|
||
|
||
### Vue 文件上传组件示例
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { supabase } from '@/lib/supabaseClient'
|
||
|
||
const uploading = ref(false)
|
||
const fileUrl = ref('')
|
||
|
||
const uploadFile = async (event) => {
|
||
try {
|
||
uploading.value = true
|
||
const file = event.target.files[0]
|
||
const fileExt = file.name.split('.').pop()
|
||
const fileName = `${Math.random()}.${fileExt}`
|
||
const filePath = `${fileName}`
|
||
|
||
const { error: uploadError } = await supabase.storage
|
||
.from('avatars')
|
||
.upload(filePath, file)
|
||
|
||
if (uploadError) throw uploadError
|
||
|
||
const { data } = supabase.storage
|
||
.from('avatars')
|
||
.getPublicUrl(filePath)
|
||
|
||
fileUrl.value = data.publicUrl
|
||
} catch (error) {
|
||
alert('上传失败: ' + error.message)
|
||
} finally {
|
||
uploading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<input type="file" @change="uploadFile" :disabled="uploading" />
|
||
<div v-if="uploading">上传中...</div>
|
||
<img v-if="fileUrl" :src="fileUrl" alt="上传的图片" />
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
## Realtime 使用
|
||
|
||
### 订阅数据变化
|
||
|
||
```javascript
|
||
// 订阅表的所有变化
|
||
const channel = supabase
|
||
.channel('public:users')
|
||
.on('postgres_changes',
|
||
{ event: '*', schema: 'public', table: 'users' },
|
||
(payload) => {
|
||
console.log('数据变化:', payload)
|
||
}
|
||
)
|
||
.subscribe()
|
||
|
||
// 订阅特定操作
|
||
const channel = supabase
|
||
.channel('public:posts')
|
||
.on('postgres_changes',
|
||
{ event: 'INSERT', schema: 'public', table: 'posts' },
|
||
(payload) => {
|
||
console.log('新增记录:', payload.new)
|
||
}
|
||
)
|
||
.on('postgres_changes',
|
||
{ event: 'UPDATE', schema: 'public', table: 'posts' },
|
||
(payload) => {
|
||
console.log('更新记录:', payload.new)
|
||
}
|
||
)
|
||
.on('postgres_changes',
|
||
{ event: 'DELETE', schema: 'public', table: 'posts' },
|
||
(payload) => {
|
||
console.log('删除记录:', payload.old)
|
||
}
|
||
)
|
||
.subscribe()
|
||
```
|
||
|
||
### 取消订阅
|
||
|
||
```javascript
|
||
const removeChannel = async () => {
|
||
await supabase.removeChannel(channel)
|
||
}
|
||
```
|
||
|
||
### Vue 实时数据组件示例
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { supabase } from '@/lib/supabaseClient'
|
||
|
||
const messages = ref([])
|
||
let channel = null
|
||
|
||
onMounted(async () => {
|
||
// 加载初始数据
|
||
const { data } = await supabase
|
||
.from('messages')
|
||
.select('*')
|
||
.order('created_at', { ascending: false })
|
||
|
||
messages.value = data || []
|
||
|
||
// 订阅实时更新
|
||
channel = supabase
|
||
.channel('public:messages')
|
||
.on('postgres_changes',
|
||
{ event: 'INSERT', schema: 'public', table: 'messages' },
|
||
(payload) => {
|
||
messages.value = [payload.new, ...messages.value]
|
||
}
|
||
)
|
||
.on('postgres_changes',
|
||
{ event: 'DELETE', schema: 'public', table: 'messages' },
|
||
(payload) => {
|
||
messages.value = messages.value.filter(m => m.id !== payload.old.id)
|
||
}
|
||
)
|
||
.subscribe()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (channel) {
|
||
supabase.removeChannel(channel)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<h2>实时消息</h2>
|
||
<ul>
|
||
<li v-for="message in messages" :key="message.id">
|
||
{{ message.content }}
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
## 完整示例
|
||
|
||
### 完整的 CRUD 示例
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import { supabase } from '@/lib/supabaseClient'
|
||
|
||
// 状态
|
||
const items = ref([])
|
||
const loading = ref(false)
|
||
const newItem = ref({ name: '', description: '' })
|
||
|
||
// 读取数据
|
||
const fetchItems = async () => {
|
||
loading.value = true
|
||
const { data, error } = await supabase
|
||
.from('items')
|
||
.select('*')
|
||
.order('created_at', { ascending: false })
|
||
|
||
if (error) {
|
||
console.error('获取数据失败:', error)
|
||
} else {
|
||
items.value = data
|
||
}
|
||
loading.value = false
|
||
}
|
||
|
||
// 创建数据
|
||
const createItem = async () => {
|
||
const { data, error } = await supabase
|
||
.from('items')
|
||
.insert([newItem.value])
|
||
.select()
|
||
|
||
if (error) {
|
||
alert('创建失败: ' + error.message)
|
||
} else {
|
||
items.value = [data[0], ...items.value]
|
||
newItem.value = { name: '', description: '' }
|
||
}
|
||
}
|
||
|
||
// 更新数据
|
||
const updateItem = async (id, updates) => {
|
||
const { data, error } = await supabase
|
||
.from('items')
|
||
.update(updates)
|
||
.eq('id', id)
|
||
.select()
|
||
|
||
if (error) {
|
||
alert('更新失败: ' + error.message)
|
||
} else {
|
||
const index = items.value.findIndex(item => item.id === id)
|
||
if (index !== -1) {
|
||
items.value[index] = data[0]
|
||
}
|
||
}
|
||
}
|
||
|
||
// 删除数据
|
||
const deleteItem = async (id) => {
|
||
if (!confirm('确认删除?')) return
|
||
|
||
const { error } = await supabase
|
||
.from('items')
|
||
.delete()
|
||
.eq('id', id)
|
||
|
||
if (error) {
|
||
alert('删除失败: ' + error.message)
|
||
} else {
|
||
items.value = items.value.filter(item => item.id !== id)
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchItems()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<h1>数据管理</h1>
|
||
|
||
<!-- 创建表单 -->
|
||
<div class="form">
|
||
<input v-model="newItem.name" placeholder="名称" />
|
||
<input v-model="newItem.description" placeholder="描述" />
|
||
<button @click="createItem">创建</button>
|
||
</div>
|
||
|
||
<!-- 列表 -->
|
||
<div v-if="loading">加载中...</div>
|
||
<ul v-else>
|
||
<li v-for="item in items" :key="item.id">
|
||
<span>{{ item.name }} - {{ item.description }}</span>
|
||
<button @click="updateItem(item.id, { name: '已更新' })">更新</button>
|
||
<button @click="deleteItem(item.id)">删除</button>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
## 错误处理
|
||
|
||
### 统一错误处理
|
||
|
||
```javascript
|
||
// src/lib/errorHandler.js
|
||
export const handleSupabaseError = (error) => {
|
||
if (!error) return null
|
||
|
||
// 认证错误
|
||
if (error.status === 401) {
|
||
console.error('认证失败,请重新登录')
|
||
// 跳转到登录页
|
||
router.push('/login')
|
||
}
|
||
|
||
// 权限错误
|
||
if (error.status === 403) {
|
||
console.error('权限不足')
|
||
}
|
||
|
||
// 网络错误
|
||
if (error.message && error.message.includes('fetch')) {
|
||
console.error('网络连接失败')
|
||
}
|
||
|
||
return error.message || '操作失败'
|
||
}
|
||
```
|
||
|
||
### 使用错误处理
|
||
|
||
```javascript
|
||
import { handleSupabaseError } from '@/lib/errorHandler'
|
||
|
||
const fetchData = async () => {
|
||
const { data, error } = await supabase
|
||
.from('users')
|
||
.select('*')
|
||
|
||
if (error) {
|
||
const errorMsg = handleSupabaseError(error)
|
||
alert(errorMsg)
|
||
return
|
||
}
|
||
|
||
// 处理成功的数据
|
||
console.log(data)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 数据库 Schema 示例
|
||
|
||
### 创建表(在 Dashboard 中执行)
|
||
|
||
```sql
|
||
-- 创建用户表
|
||
CREATE TABLE users (
|
||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||
email TEXT UNIQUE NOT NULL,
|
||
name TEXT,
|
||
avatar_url TEXT,
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
|
||
-- 创建帖子表
|
||
CREATE TABLE posts (
|
||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
content TEXT,
|
||
published BOOLEAN DEFAULT false,
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
|
||
-- 启用 RLS (Row Level Security)
|
||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
||
|
||
-- 创建策略
|
||
CREATE POLICY "用户可以查看所有用户" ON users
|
||
FOR SELECT USING (true);
|
||
|
||
CREATE POLICY "用户只能更新自己的数据" ON users
|
||
FOR UPDATE USING (auth.uid() = id);
|
||
|
||
CREATE POLICY "所有人可以查看已发布的帖子" ON posts
|
||
FOR SELECT USING (published = true);
|
||
|
||
CREATE POLICY "用户可以创建自己的帖子" ON posts
|
||
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||
|
||
CREATE POLICY "用户可以更新自己的帖子" ON posts
|
||
FOR UPDATE USING (auth.uid() = user_id);
|
||
```
|
||
|
||
---
|
||
|
||
## 📚 更多资源
|
||
|
||
- [Supabase 官方文档](https://supabase.com/docs)
|
||
- [Supabase JS 客户端文档](https://supabase.com/docs/reference/javascript)
|
||
- [Vue 3 官方文档](https://vuejs.org/)
|
||
|
||
---
|
||
|
||
## ⚠️ 注意事项
|
||
|
||
1. **ANON_KEY 是公开的**:可以在前端代码中使用
|
||
2. **SERVICE_ROLE_KEY 必须保密**:只能在后端使用
|
||
3. **使用 RLS (Row Level Security)**:保护数据安全
|
||
4. **Dashboard 访问**:只能通过内网 `http://100.64.0.2:18000` 访问
|
||
5. **API 端点**:通过 `https://amiap.hzau.edu.cn/supa` 访问所有服务
|
||
|
||
---
|
||
|
||
## 🎯 下一步
|
||
|
||
1. 在 Dashboard 中创建数据库表
|
||
2. 配置 RLS 策略
|
||
3. 在 Vue 应用中集成 Supabase
|
||
4. 开发认证、CRUD、文件上传等功能
|
||
5. 测试实时订阅功能
|
||
|
||
---
|
||
|
||
## 📦 S3 对象存储配置与使用
|
||
|
||
### 概述
|
||
|
||
Supabase Storage 已配置使用 **MinIO** 作为 S3 兼容的对象存储后端。所有文件都会保存在宿主机的 `/vol1/1000/s3` 目录。
|
||
|
||
### 🔧 启动 S3 存储服务
|
||
|
||
```bash
|
||
cd /vol1/1000/docker_server/traefik/supabase-stack
|
||
docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||
```
|
||
|
||
**说明**:
|
||
- `docker-compose.yml`: 主配置文件(包含所有 Supabase 服务)
|
||
- `docker-compose.s3.yml`: MinIO 配置文件(在 `/vol1/1000/docker_server/traefik/supabase-stack/` 目录)
|
||
- 所有对象文件实际存储在:`/vol1/1000/s3/stub/<bucket_name>/...`
|
||
|
||
---
|
||
|
||
### 🌐 S3 访问端点
|
||
|
||
#### 统一 S3 端点(推荐)
|
||
|
||
```
|
||
https://amiap.hzau.edu.cn/supa/storage/v1/s3
|
||
```
|
||
|
||
这是标准的 Supabase Storage S3 协议端点,完全兼容 AWS S3 API。
|
||
|
||
#### MinIO 管理控制台(可选)
|
||
|
||
```
|
||
http://100.64.0.2:9001
|
||
用户名: supa-storage
|
||
密码: secret519521
|
||
```
|
||
|
||
---
|
||
|
||
### 🔑 S3 认证密钥
|
||
|
||
#### 1. S3 协议访问密钥(推荐用于应用)
|
||
|
||
用于通过 Supabase Storage 的 S3 端点访问:
|
||
|
||
```bash
|
||
# 从 .env 文件
|
||
S3_PROTOCOL_ACCESS_KEY_ID=supa-protocol-key
|
||
S3_PROTOCOL_ACCESS_KEY_SECRET=supa-protocol-secret
|
||
S3_PROTOCOL_REGION=stub
|
||
```
|
||
|
||
#### 2. MinIO 根密钥(内部使用)
|
||
|
||
用于 storage 服务连接 MinIO,一般不直接使用:
|
||
|
||
```bash
|
||
MINIO_ROOT_USER=supa-storage
|
||
MINIO_ROOT_PASSWORD=secret1234
|
||
```
|
||
|
||
---
|
||
|
||
### 💻 客户端配置示例
|
||
|
||
#### Python (boto3)
|
||
|
||
```python
|
||
import boto3
|
||
from botocore.client import Config
|
||
|
||
# 配置 S3 客户端连接到 Supabase Storage
|
||
s3_client = boto3.client(
|
||
's3',
|
||
endpoint_url='https://amiap.hzau.edu.cn/supa/storage/v1/s3',
|
||
aws_access_key_id='supa-protocol-key',
|
||
aws_secret_access_key='supa-protocol-secret',
|
||
region_name='stub',
|
||
config=Config(
|
||
signature_version='s3v4',
|
||
s3={'addressing_style': 'path'}
|
||
),
|
||
verify=True # HTTPS 验证
|
||
)
|
||
|
||
# 列出所有 buckets
|
||
response = s3_client.list_buckets()
|
||
for bucket in response['Buckets']:
|
||
print(f"Bucket: {bucket['Name']}")
|
||
|
||
# 上传文件
|
||
with open('example.jpg', 'rb') as file:
|
||
s3_client.upload_fileobj(
|
||
file,
|
||
'my-bucket', # bucket 名称
|
||
'uploads/example.jpg' # 对象路径
|
||
)
|
||
|
||
# 下载文件
|
||
s3_client.download_file(
|
||
'my-bucket',
|
||
'uploads/example.jpg',
|
||
'downloaded.jpg'
|
||
)
|
||
|
||
# 生成预签名 URL(临时访问链接)
|
||
url = s3_client.generate_presigned_url(
|
||
'get_object',
|
||
Params={'Bucket': 'my-bucket', 'Key': 'uploads/example.jpg'},
|
||
ExpiresIn=3600 # 1小时过期
|
||
)
|
||
print(f"Presigned URL: {url}")
|
||
```
|
||
|
||
#### JavaScript/Node.js (AWS SDK v3)
|
||
|
||
```javascript
|
||
import { S3Client, ListBucketsCommand, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
|
||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
||
import fs from 'fs'
|
||
|
||
// 配置 S3 客户端
|
||
const s3Client = new S3Client({
|
||
endpoint: 'https://amiap.hzau.edu.cn/supa/storage/v1/s3',
|
||
region: 'stub',
|
||
credentials: {
|
||
accessKeyId: 'supa-protocol-key',
|
||
secretAccessKey: 'supa-protocol-secret',
|
||
},
|
||
forcePathStyle: true, // 使用路径风格访问
|
||
})
|
||
|
||
// 列出 buckets
|
||
const listBuckets = async () => {
|
||
const command = new ListBucketsCommand({})
|
||
const response = await s3Client.send(command)
|
||
console.log('Buckets:', response.Buckets)
|
||
}
|
||
|
||
// 上传文件
|
||
const uploadFile = async () => {
|
||
const fileContent = fs.readFileSync('example.jpg')
|
||
|
||
const command = new PutObjectCommand({
|
||
Bucket: 'my-bucket',
|
||
Key: 'uploads/example.jpg',
|
||
Body: fileContent,
|
||
ContentType: 'image/jpeg',
|
||
})
|
||
|
||
const response = await s3Client.send(command)
|
||
console.log('Upload success:', response)
|
||
}
|
||
|
||
// 生成预签名 URL
|
||
const getPresignedUrl = async () => {
|
||
const command = new GetObjectCommand({
|
||
Bucket: 'my-bucket',
|
||
Key: 'uploads/example.jpg',
|
||
})
|
||
|
||
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 })
|
||
console.log('Presigned URL:', url)
|
||
return url
|
||
}
|
||
|
||
// 执行
|
||
await listBuckets()
|
||
await uploadFile()
|
||
await getPresignedUrl()
|
||
```
|
||
|
||
#### PostgreSQL Foreign Data Wrapper (FDW)
|
||
|
||
在 PostgreSQL 中直接查询 S3 对象:
|
||
|
||
```sql
|
||
-- 1. 启用扩展
|
||
CREATE EXTENSION IF NOT EXISTS wrappers CASCADE;
|
||
|
||
-- 2. 创建 S3 服务器连接
|
||
CREATE SERVER s3_server
|
||
FOREIGN DATA WRAPPER s3_wrapper
|
||
OPTIONS (
|
||
aws_access_key_id 'supa-protocol-key',
|
||
aws_secret_access_key 'supa-protocol-secret',
|
||
aws_region 'stub',
|
||
endpoint_url 'https://amiap.hzau.edu.cn/supa/storage/v1/s3',
|
||
path_style_url 'true'
|
||
);
|
||
|
||
-- 3. 创建外部表映射到 S3 对象(例如 Parquet 文件)
|
||
CREATE FOREIGN TABLE my_s3_table (
|
||
id bigint,
|
||
name text,
|
||
created_at timestamp
|
||
)
|
||
SERVER s3_server
|
||
OPTIONS (
|
||
uri 's3://my-bucket/data/file.parquet',
|
||
format 'parquet'
|
||
);
|
||
|
||
-- 4. 查询 S3 数据
|
||
SELECT * FROM my_s3_table WHERE created_at > NOW() - INTERVAL '7 days';
|
||
```
|
||
|
||
#### AWS CLI
|
||
|
||
```bash
|
||
# 配置 AWS CLI
|
||
export AWS_ACCESS_KEY_ID=supa-protocol-key
|
||
export AWS_SECRET_ACCESS_KEY=supa-protocol-secret
|
||
export AWS_DEFAULT_REGION=stub
|
||
|
||
# 或创建 AWS 配置文件 ~/.aws/config
|
||
# [profile supabase]
|
||
# region = stub
|
||
# output = json
|
||
|
||
# ~/.aws/credentials
|
||
# [supabase]
|
||
# aws_access_key_id = supa-protocol-key
|
||
# aws_secret_access_key = supa-protocol-secret
|
||
|
||
# 列出 buckets
|
||
aws s3 ls --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
|
||
|
||
# 列出 bucket 中的对象
|
||
aws s3 ls s3://my-bucket/ --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
|
||
|
||
# 上传文件
|
||
aws s3 cp example.jpg s3://my-bucket/uploads/ \
|
||
--endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
|
||
|
||
# 下载文件
|
||
aws s3 cp s3://my-bucket/uploads/example.jpg downloaded.jpg \
|
||
--endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
|
||
|
||
# 同步目录
|
||
aws s3 sync ./local-dir s3://my-bucket/backup/ \
|
||
--endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
|
||
```
|
||
|
||
#### Vue 3 + Supabase Client(推荐)
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { supabase } from '@/lib/supabaseClient'
|
||
|
||
const uploading = ref(false)
|
||
const uploadedUrl = ref('')
|
||
|
||
// 上传文件到 S3
|
||
const uploadToS3 = async (event) => {
|
||
try {
|
||
uploading.value = true
|
||
const file = event.target.files[0]
|
||
const fileExt = file.name.split('.').pop()
|
||
const fileName = `${Math.random()}.${fileExt}`
|
||
const filePath = `uploads/${fileName}`
|
||
|
||
// Supabase Storage 会自动使用配置的 S3 后端(MinIO)
|
||
const { data, error } = await supabase.storage
|
||
.from('my-bucket') // bucket 名称
|
||
.upload(filePath, file, {
|
||
cacheControl: '3600',
|
||
upsert: false
|
||
})
|
||
|
||
if (error) throw error
|
||
|
||
// 获取公开 URL
|
||
const { data: urlData } = supabase.storage
|
||
.from('my-bucket')
|
||
.getPublicUrl(filePath)
|
||
|
||
uploadedUrl.value = urlData.publicUrl
|
||
console.log('文件已上传到 S3:', uploadedUrl.value)
|
||
} catch (error) {
|
||
console.error('上传失败:', error.message)
|
||
} finally {
|
||
uploading.value = false
|
||
}
|
||
}
|
||
|
||
// 从 S3 下载文件
|
||
const downloadFromS3 = async (filePath) => {
|
||
const { data, error } = await supabase.storage
|
||
.from('my-bucket')
|
||
.download(filePath)
|
||
|
||
if (error) {
|
||
console.error('下载失败:', error)
|
||
return
|
||
}
|
||
|
||
// 创建下载链接
|
||
const url = URL.createObjectURL(data)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = filePath.split('/').pop()
|
||
a.click()
|
||
}
|
||
|
||
// 列出 bucket 中的文件
|
||
const listFiles = async () => {
|
||
const { data, error } = await supabase.storage
|
||
.from('my-bucket')
|
||
.list('uploads/', {
|
||
limit: 100,
|
||
offset: 0,
|
||
sortBy: { column: 'created_at', order: 'desc' }
|
||
})
|
||
|
||
if (error) {
|
||
console.error('列表失败:', error)
|
||
return
|
||
}
|
||
|
||
console.log('S3 文件列表:', data)
|
||
return data
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<h2>S3 文件上传</h2>
|
||
<input type="file" @change="uploadToS3" :disabled="uploading" />
|
||
<div v-if="uploading">上传中...</div>
|
||
<div v-if="uploadedUrl">
|
||
<p>上传成功!</p>
|
||
<a :href="uploadedUrl" target="_blank">查看文件</a>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
### 🗂️ 数据存储位置
|
||
|
||
**所有上传的文件实际保存在**:
|
||
|
||
```
|
||
/vol1/1000/s3/stub/<bucket_name>/<object_path>
|
||
```
|
||
|
||
示例:
|
||
```
|
||
/vol1/1000/s3/
|
||
└── stub/
|
||
├── my-bucket/
|
||
│ ├── uploads/
|
||
│ │ ├── image1.jpg
|
||
│ │ └── image2.png
|
||
│ └── documents/
|
||
│ └── report.pdf
|
||
└── avatars/
|
||
└── user123.jpg
|
||
```
|
||
|
||
---
|
||
|
||
### 🔒 安全建议
|
||
|
||
1. **生产环境密钥**:修改 `.env` 中的默认密钥
|
||
2. **HTTPS 访问**:外部访问使用 HTTPS 端点
|
||
3. **Bucket 权限**:合理配置 RLS 策略
|
||
4. **MinIO 控制台**:仅内网访问(端口 9001)
|
||
|
||
---
|
||
|
||
### ❓ 常见问题
|
||
|
||
**Q: 文件存在哪里?**
|
||
A: 所有文件物理存储在 `/vol1/1000/s3/stub/<bucket_name>/`
|
||
|
||
**Q: 如何备份数据?**
|
||
A: 直接备份 `/vol1/1000/s3` 目录即可
|
||
|
||
**Q: 可以直接访问 MinIO 吗?**
|
||
A: 可以,但推荐使用 Supabase Storage 的 S3 端点(更安全)
|
||
|
||
**Q: 如何迁移现有文件到 S3?**
|
||
A: 使用 AWS CLI 或 Python 脚本批量上传到对应 bucket
|
||
|
||
---
|
||
|
||
### 📝 在 Dashboard 中创建 Bucket
|
||
|
||
1. 访问 Dashboard: `http://100.64.0.2:18000`
|
||
2. 登录后进入 **Storage** 页面
|
||
3. 点击 **New Bucket**
|
||
4. 输入 bucket 名称(例如:`my-bucket`)
|
||
5. 配置访问权限(Public/Private)
|
||
6. 创建完成后,文件会保存在 `/vol1/1000/s3/stub/my-bucket/`
|
||
|
||
---
|
||
|
||
### ⚙️ 配置文件说明
|
||
|
||
#### .env 配置
|
||
|
||
```bash
|
||
# MinIO 内部认证
|
||
MINIO_ROOT_USER=supa-storage
|
||
MINIO_ROOT_PASSWORD=secret1234
|
||
|
||
# S3 协议访问密钥(用于外部客户端)
|
||
S3_PROTOCOL_ACCESS_KEY_ID=supa-protocol-key
|
||
S3_PROTOCOL_ACCESS_KEY_SECRET=supa-protocol-secret
|
||
S3_PROTOCOL_REGION=stub
|
||
|
||
# S3 配置
|
||
GLOBAL_S3_BUCKET=stub
|
||
GLOBAL_S3_ENDPOINT=http://minio:9000
|
||
GLOBAL_S3_PROTOCOL=http
|
||
GLOBAL_S3_FORCE_PATH_STYLE=true
|
||
AWS_DEFAULT_REGION=stub
|
||
```
|
||
|
||
#### docker-compose.s3.yml
|
||
|
||
```yaml
|
||
services:
|
||
minio:
|
||
image: minio/minio
|
||
ports:
|
||
- "9000:9000" # S3 API
|
||
- "9001:9001" # Web 控制台
|
||
environment:
|
||
MINIO_ROOT_USER: supa-storage
|
||
MINIO_ROOT_PASSWORD: secret1234
|
||
command: server --console-address ":9001" /data
|
||
volumes:
|
||
- /vol1/1000/s3:/data:z # 数据存储位置
|
||
|
||
minio-createbucket:
|
||
image: minio/mc
|
||
depends_on:
|
||
minio:
|
||
condition: service_healthy
|
||
entrypoint: >
|
||
/bin/sh -c "
|
||
/usr/bin/mc alias set supa-minio http://minio:9000 supa-storage secret1234;
|
||
/usr/bin/mc mb supa-minio/stub;
|
||
exit 0;
|
||
"
|
||
```
|
||
|
||
---
|
||
|
||
### 🔧 验证 S3 配置
|
||
|
||
#### 1. 检查 MinIO 服务状态
|
||
|
||
```bash
|
||
docker compose ps minio
|
||
```
|
||
|
||
#### 2. 使用 MinIO Client 验证
|
||
|
||
```bash
|
||
# 进入 MinIO 容器
|
||
docker exec -it $(docker ps -qf "name=minio") sh
|
||
|
||
# 配置 mc 别名
|
||
mc alias set local http://localhost:9000 supa-storage secret1234
|
||
|
||
# 列出 buckets
|
||
mc ls local/
|
||
|
||
# 查看 stub bucket
|
||
mc ls local/stub/
|
||
|
||
# 退出
|
||
exit
|
||
```
|
||
|
||
#### 3. 测试 S3 API 端点
|
||
|
||
```bash
|
||
# 测试 endpoint 是否可访问
|
||
curl -I https://amiap.hzau.edu.cn/supa/storage/v1/s3
|
||
```
|
||
|
||
---
|
||
|
||
### 📚 相关资源
|
||
|
||
- [Supabase Storage 文档](https://supabase.com/docs/guides/storage)
|
||
- [MinIO 文档](https://min.io/docs/)
|
||
- [AWS S3 API 文档](https://docs.aws.amazon.com/s3/)
|
||
- [boto3 文档](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) |