从微信云开发迁移到 Supabase:一个小型小程序的后端重构之路

5 阅读7分钟

每日一句诗-小程序二维码.jpg

## 引言

作为一名独立开发者,我维护着一个名为「每日一诗」的微信小程序。这是一个非常简单的应用——每天为用户推荐一首古诗词,支持打卡和徽章功能。

就在最近,我面临一个棘手的问题:微信云开发的环境即将到期

经过调研,我决定将后端从微信云开发迁移到 Supabase。整个迁移过程耗时约一周,踩了不少坑,也收获了很多经验。今天这篇文章,我想把这段经历分享出来,希望能为有类似需求的朋友提供一些参考。


为什么放弃微信云开发?

微信云开发是一个优秀的方案,它让小程序开发者可以快速搭建后端服务,无需运维,开箱即用。但对于我的场景,它存在几个难以接受的限制:

1. 环境到期且不支持迁移

微信云开发的环境存在有效期,到期后数据会怎样?官方文档语焉不详。更关键的是,即使我重新购买环境,也无法平滑迁移数据。这意味着我要么接受数据丢失的风险,要么在到期前手动导出所有数据——对于一个懒癌晚期开发者来说,这太痛苦了。

2. 费用逐年上涨

云开发的免费额度在逐年缩减,而我的小程序虽然用户量不大,但访问量却在缓慢增长。去年还能白嫖的功能,今年可能就要开始付费了。虽然费用不高,但作为一个没有任何商业化的小工具,我不想每年为它支付任何费用。

3. 微信生态的绑定焦虑

把所有用户数据、业务逻辑都绑定在微信生态里,让我隐隐感到不安。如果有一天微信调整了政策,或者我想要开发其他平台的应用(比如 iOS、H5),这些数据和服务将很难迁移出来。

基于以上考虑,我开始寻找一个开源、可移植、免费的替代方案。


为什么选择 Supabase?

在调研了多个后端即服务(BaaS)平台后,我最终锁定了 Supabase。选择它的理由很简单:

开源 + 免费 + 全球化

Supabase 是 Firebase 的开源替代品,基于 PostgreSQL 数据库,提供认证、存储、Edge Functions 等功能。它的免费额度对于我的小程序来说完全够用——每月 500MB 数据库、50MB 文件存储、50 万次 Edge Function 调用。更重要的是,它是开源的,即使哪天 Supabase 公司倒闭了,我也可以自行部署,不存在被「绑架」的风险。

技术栈匹配度高

Supabase 使用 Deno 编写 Edge Functions,对于前端开发者来说非常友好。我可以用 TypeScript 快速编写后端逻辑,无需学习新的语言和框架。

社区活跃,文档完善

Supabase 的文档写得非常清晰,社区也很活跃,遇到问题很容易找到解决方案。相比之下,微信云开发的文档虽然也不错,但很多高级用法需要自己摸索。


迁移方案概述

整个迁移过程可以分为三个阶段:

第一阶段:数据库设计

我把原来的微信云数据库(MongoDB 风格)的数据迁移到了 Supabase 的 PostgreSQL。主要涉及三张表:

  • users:用户信息,包括 openid、昵称、连续打卡天数、徽章等
  • check_ins:打卡记录,关联用户,存储诗词内容
  • badges:徽章定义,包括名称、描述、获取条件等

在 Supabase 中,我开启了 Row Level Security(RLS)策略,确保用户只能访问自己的数据,安全性得到了保障。

第二阶段:云函数重构

原来使用微信云函数实现的业务逻辑,现在迁移到 Supabase Edge Functions:

功能原微信云函数新 Edge Function
用户登录userwechat-auth
打卡checkIncheck-in
获取用户数据getUserDataget-user-data
邮箱登录-email-login

第三阶段:小程序端适配

小程序的修改相对简单,只需要把请求地址从微信云函数改成 Supabase Edge Function,同时调整一下请求参数和返回数据的解析逻辑。核心业务逻辑完全不需要改变。


核心代码实现

这里展示几个关键功能的精简版实现,希望能为有类似需求的朋友提供参考。

1. 微信静默登录(Edge Function)

// supabase/functions/wechat-auth/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })

  const { code } = await req.json()
  const { data: { env } } = await supabase.auth.getEnv()

  // 调用微信 API 获取 openid
  const wxRes = await fetch(
    `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${Deno.env.get('APPID')}&secret=${Deno.env.get('SECRET')}&code=${code}&grant_type=authorization_code`
  )
  const tokenData = await wxRes.json()

  if (tokenData.errcode) {
    return new Response(JSON.stringify({ error: tokenData.errmsg }), {
      status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }
    })
  }

  const { openid } = tokenData

  // 查询或创建用户
  const { data: user, error } = await supabase
    .from('users')
    .select('*')
    .eq('openid', openid)
    .single()

  if (!user && !error) {
    // 创建新用户
    const { data: newUser } = await supabase
      .from('users')
      .insert({ openid, nickname: '诗词爱好者', badges: [] })
      .select()
      .single()
    return new Response(JSON.stringify({ success: true, user: newUser }), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' }
    })
  }

  return new Response(JSON.stringify({ success: true, user }), {
    headers: { ...corsHeaders, 'Content-Type': 'application/json' }
  })
})

2. 打卡逻辑(Edge Function)

// supabase/functions/check-in/index.ts
Deno.serve(async (req) => {
  const { openid, poem_content, poem_title, poem_author, poem_dynasty } = await req.json()

  // 获取用户
  const { data: user } = await supabase.from('users').select('*').eq('openid', openid).single()
  const today = new Date().toISOString().split('T')[0]

  // 检查今日是否已打卡
  if (user.last_check_in_date === today) {
    return new Response(JSON.stringify({ success: false, message: '今日已打卡' }))
  }

  // 计算连续打卡天数
  let continuousDays = 1
  if (user.last_check_in_date) {
    const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]
    if (user.last_check_in_date === yesterday) {
      continuousDays = user.continuous_check_in_days + 1
    }
  }

  // 记录打卡
  await supabase.from('check_ins').insert({
    user_id: user.id, poem_content, poem_title, poem_author, poem_dynasty, check_in_date: today
  })

  // 更新用户数据
  await supabase.from('users').update({
    continuous_check_in_days: continuousDays,
    last_check_in_date: today,
    updated_at: new Date().toISOString()
  }).eq('id', user.id)

  return new Response(JSON.stringify({ success: true, continuous_days: continuousDays }))
})

3. 小程序端调用

// pages/index/index.js
const SUPABASE_URL = 'https://xxx.supabase.co'
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

function callSupabaseFunction(functionName, data) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: `${SUPABASE_URL}/functions/v1/${functionName}`,
      method: 'POST',
      header: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
      },
      data,
      success: (res) => res.data.success ? resolve(res.data) : reject(new Error(res.data.error)),
      fail: reject
    })
  })
}

// 打卡
async function handleCheckIn() {
  const res = await callSupabaseFunction('check-in', {
    openid: this.data.openid,
    poem_content: this.data.poemData.content,
    poem_title: this.data.poemData.title
  })
  if (res.success) {
    wx.showToast({ title: '打卡成功!', icon: 'success' })
  }
}

踩坑记录与解决方案

迁移过程中遇到了几个有意思的问题,这里分享出来,希望读者能避开这些坑。

问题一:跨域请求失败

本地开发时,Edge Function 部署到 Supabase 服务器上,请求时报了 CORS 错误。解决方案是在 Edge Function 中添加正确的 CORS 头:

const corsHeaders = {
  'Access-Control-Allow-Origin': '*', // 生产环境建议指定具体域名
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

问题二:时区处理

PostgreSQL 默认存储的是 UTC 时间,而中国时区是 UTC+8。在计算「今日是否已打卡」时,我一开始直接用 new Date() 获取当前时间,导致打卡判断出现偏差。解决方案是统一将日期字符串转为 UTC 后再比较,或者在数据库查询时使用 timezone('Asia/Shanghai', now())

问题三:徽章并发问题

用户快速点击打卡按钮时,可能触发多次打卡请求,导致徽章被重复发放。解决方案是在 Edge Function 中使用数据库事务,或者在用户表上添加唯一索引,确保同一用户的打卡操作串行执行。

问题四:部署后环境变量不生效

在本地测试时,我把微信 AppSecret 写死在代码里,上传 Edge Function 后忘记在 Supabase Dashboard 中配置环境变量,导致所有请求都失败了。教训是:敏感信息一定要通过环境变量注入,不要心存侥幸。


迁移后的效果

迁移完成后,我对比了前后两个版本的体验:

指标微信云开发Supabase
响应时间~200ms~300ms
费用免费(即将收费)免费
数据可控性绑定微信生态开源可移植
开发体验一般良好

响应时间略有增加,但用户基本感知不到。费用从「即将付费」变成了「永久免费」,数据也从被微信「绑架」变成了完全由自己掌控。


总结与建议

如果你也在维护一个小程序,并且对微信云开发的限制感到困扰,我建议可以考虑 Supabase 作为替代方案。它的免费额度足够支撑大多数小型项目,开源的特性也让你在未来有更多选择。

当然,迁移是有成本的。如果你现在的小程序用户量很小,迁移的收益可能不足以抵消投入的精力。但如果你对数据可控性有追求,或者想要在未来支持多平台,Supabase 绝对值得一试。

最后,分享三点建议:

  1. 迁移前做好数据备份:无论是导出 JSON 还是写脚本迁移,都要确保数据不丢失。
  2. 保留旧版本一段时间:上线新版本后,观察一段时间再下线旧版本,给自己留条后路。
  3. 文档要写清楚:代码注释、部署流程、配置说明,都要写得清清楚楚,方便以后维护。

祝各位开发顺利,代码无 Bug!