Logo花火漫画开发文档

客户端上传流程

头像与 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()}`

On this page