客户端上传流程
头像与 Banner 的客户端处理、上传约定和代码示例
上传流程
用户选择图片
↓
客户端格式转换(→ WebP)
↓
重命名为 {user_uuid}.webp
↓
调用 Supabase Storage upsert
↓
旧文件自动被覆盖
↓
返回公开 URL格式转换和重命名必须在客户端完成,服务端不做任何图片处理。这是为了降低后端负载和存储成本。
客户端处理步骤
VIP 检查:在进入上传界面前,先检查用户 profiles.vip_expiration_date 是否未过期。非 VIP 用户不展示上传入口
图片选择:用户从相册或相机选择一张图片
裁剪(可选):头像建议 1:1 正方形裁剪,Banner 建议 3:1 宽幅裁剪
格式转换:将图片编码为 WebP 格式,质量建议 80%,控制输出在 2 MB 以内
文件命名:将文件名设为当前用户的 UUID + .webp 后缀
上传:调用 Supabase Storage API 的 upload 方法(启用 upsert: true),同名文件自动覆盖
代码示例
suspend fun uploadAvatar(bitmap: Bitmap, userId: String) {
// 1. 转换为 WebP
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 80, outputStream)
val webpBytes = outputStream.toByteArray()
// 2. 检查大小
require(webpBytes.size <= 2 * 1024 * 1024) { "图片超过 2MB 限制" }
// 3. 上传(文件名 = UUID.webp)
val path = "${userId}.webp"
supabase.storage
.from("avatars")
.upload(
path = path,
data = webpBytes,
upsert = true // 覆盖旧文件
)
}async function uploadAvatar(file: File, userId: string) {
// 1. 转换为 WebP(使用 Canvas API)
const bitmap = await createImageBitmap(file)
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)
const ctx = canvas.getContext('2d')!
ctx.drawImage(bitmap, 0, 0)
const webpBlob = await canvas.convertToBlob({
type: 'image/webp',
quality: 0.8,
})
// 2. 检查大小
if (webpBlob.size > 2 * 1024 * 1024) {
throw new Error('图片超过 2MB 限制')
}
// 3. 上传(文件名 = UUID.webp)
const { error } = await supabase.storage
.from('avatars')
.upload(`${userId}.webp`, webpBlob, {
contentType: 'image/webp',
upsert: true, // 覆盖旧文件
})
if (error) throw error
}覆盖机制说明
upsert: true 是关键参数。Supabase Storage 的行为:
| upsert | 文件已存在 | 结果 |
|---|---|---|
false | 是 | 返回 409 Conflict |
true | 是 | 覆盖旧文件,同一路径 |
true | 否 | 正常创建 |
由于文件名固定为 {uuid}.webp,每次上传都会覆盖旧头像,不会产生多余文件,存储空间保持稳定。
CDN 缓存注意事项
头像和 Banner 的公开 URL 不变(始终是 {uuid}.webp),但文件内容会变。需要注意:
- Supabase Storage 的公开 URL 默认不带强缓存头
- 如果在前端做了本地缓存,更新头像后需要加
?t={timestamp}参数强制刷新 - Android 客户端使用 Coil / Glide 时,可通过
memoryCachePolicy(DISABLED)在头像更新后立即刷新
// Web 端获取最新头像(避免缓存)
const avatarUrl = `${publicUrl}?t=${Date.now()}`