返回博客列表
技术文章Supabase微信小程序数据建模云函数

数据建模与 Supabase Functions 全解

2026年04月🇨🇳 中文

在掌握了身份认证与行级安全(RLS)之后,我们正式进入实际业务的开发。

为了让枯燥的数据库理论变得直观,本文将贯穿一个完全虚拟的演示级业务场景:开发一个“ProjectX 灵感协作小程序”(一个允许用户发布灵感卡片、进行实时脑暴和知识打赏的平台)。我们将从最基础的“设计一张灵感表”开始,遇到多表更新问题时引入 Database Functions,在接入外部微信打赏时引入 Edge Functions,一步步搭建出坚如磐石的后端架构。


夯实地基:核心表的数据建模

任何伟大的应用都始于扎实的数据模型。在 Supabase 中,业务表都建立在 public schema 下。让我们来设计核心的 ideas(灵感卡片)表。

1. 为什么坚决使用 UUID 作为主键?

很多新手喜欢用自增整数(SERIAL)作为表的主键 ID,比如 id: 1001。这在实际商业项目中是极度危险的:

  • 暴露商业机密:对手只需每天注册一个账号,看看卡片 ID 增长了多少,就能精准推算你的日活跃度。
  • 遭受枚举攻击:黑客写个脚本,遍历 ID 从 1 到 10000 就能轻松爬取你全站的内容。

最佳实践:始终使用 UUID v4。在建表时指定 id UUID DEFAULT gen_random_uuid() PRIMARY KEY,它不仅绝对随机,还能在未来的分库分表或分布式数据合并中彻底杜绝 ID 冲突。

2. 用户关联与自动清理(级联删除)

每一张灵感卡片必须绑定创作者。我们的外键应该直接关联到 Supabase 底层负责身份管理的 auth.users 表:

author_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE

这里的魔法在于 ON DELETE CASCADE。当某个用户注销账号时,底层 auth.users 里的记录被删除,数据库会自动帮你删掉这个用户发布的所有卡片。你不需要在应用层写任何复杂的定时清理脚本,完美符合隐私法规(如 GDPR)的“被遗忘权”。

3. 状态控制:枚举还是 CHECK?

灵感卡片的状态通常有:草稿(draft)、已发布(published)、已归档(archived)。

  • 如果你确信状态极少变更:使用 PostgreSQL 原生枚举(CREATE TYPE idea_status AS ENUM ('draft', 'published'))。好处是强类型,但致命缺点是枚举值一旦创建,无法删除,也很难修改顺序
  • 对于敏捷开发的业务(推荐):使用 TEXT 结合 CHECK 约束。
    status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived'))
    
    如果你明天想加一个“审核中”状态,只需 ALTER TABLE 替换一下约束即可,极其轻量灵活。

自动化状态追踪:使用 Trigger (触发器)

灵感内容在被反复修改时,我们希望前端能精确显示“最近更新时间”。如果我们依赖前端在 UPDATE 请求里传入当前时间,那不仅繁琐,而且容易被恶意篡改。

这就是**数据库触发器(Trigger)**登场的完美时机。触发器能监听表的数据变更,并在最底层自动执行动作。

首先,我们定义一个可复用的函数,它的唯一作用就是把 updated_at 刷新为当前时间:

CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();  -- NEW 代表即将被写入的那行新数据
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

然后,我们将这个触发器挂载到 ideas 表上:

CREATE TRIGGER set_ideas_updated_at
  BEFORE UPDATE ON ideas
  FOR EACH ROW
  EXECUTE FUNCTION update_updated_at();

现在,无论用户怎么修改卡片内容,哪怕是在 Supabase Dashboard 里手动改一行数据,updated_at 都会精准无误地更新,完全防篡改。


什么是 Supabase Functions?

当应用从“简单的 CRUD”进化到“复杂的业务结算”时,纯前端的逻辑已经捉襟见肘了。试想一下:发布一个灵感不仅要存入 ideas 表,还要写入 tags 表,还要给用户增加活跃积分。如果前端网断了,导致前两步成功、加积分失败怎么办?

我们需要服务端的算力。在 Supabase 生态中,服务端函数(Functions)分为两大互补的阵营:

架构选型口诀:数据密集型的强一致事务交给 Database,网络交互与生态密集型任务交给 Edge。


收敛复杂事务:Database Functions 实战

让我们解决刚才提到的“发卡片+打标签+加积分”难题。如果在应用层执行 3 次 API 请求,这就是典型的非原子操作风险。

我们将这三步封装进一个原生的 Database Function。它直接在数据库内存中执行,没有往返网络延迟,最重要的是它天然包裹在一个事务(Transaction)里,要么全成功,要么全回滚

CREATE OR REPLACE FUNCTION publish_idea(
  p_title TEXT,
  p_content TEXT,
  p_tags TEXT[]
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY INVOKER  -- 重点:以调用者权限执行,依然受 RLS 保护
AS $$
DECLARE
  v_idea_id UUID;
  v_user_id UUID := auth.uid();  -- 自动解析当前登录用户
BEGIN
  -- 1. 创建灵感卡片
  INSERT INTO ideas (title, content, author_id)
  VALUES (p_title, p_content, v_user_id)
  RETURNING id INTO v_idea_id;

  -- 2. 写入标签(数组展开)
  INSERT INTO idea_tags (idea_id, tag_name)
  SELECT v_idea_id, unnest(p_tags);

  -- 3. 增加活跃积分
  UPDATE profiles SET points = points + 5 WHERE id = v_user_id;

  RETURN v_idea_id;
END;
$$;

在小程序端,我们只需要通过 RPC(远程过程调用)极其优雅地呼叫它一次:

const { data: ideaId, error } = await supabase
  .rpc('publish_idea', {
    p_title: '量子计算架构图草案',
    p_content: '关于超导量子比特的纠错机制构想...',
    p_tags: ['量子计算', '底层架构'],
  });

跨越数据库边界:Edge Functions 进阶

现在,有其他读者看到了这个极具启发性的灵感,决定发起微信打赏(支付赞助)。 微信支付的铁律是:签名生成、统一下单等核心操作,绝不能把商户密钥暴露在客户端。 并且,微信的回调(Webhook)必须发送给一个拥有公网 URL 的服务器。

这就是 Edge Functions 发挥威力的地方。它是独立于数据库的轻量级后端 HTTP 节点,使用 TypeScript 编写。

由于它可以自由引入 NPM 生态包,我们在编写处理微信打赏的 Edge Function 时,可以游刃有余:

// supabase/functions/wechat-reward/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import WechatPay from "npm:wechatpay-node-v3" // 直接引入庞大的 npm 生态

serve(async (req) => {
  // 1. 从环境变量安全读取商户私钥(绝不泄露给客户端)
  const privateKey = Deno.env.get("WECHAT_PRIVATE_KEY");
  
  // 2. 接收小程序传来的卡片 ID,验证金额
  const { ideaId, amount } = await req.json();
  
  // 3. 向微信服务器发起安全的 Server-to-Server 统一下单请求
  // ...
  
  return new Response(JSON.stringify(paySignData), {
    headers: { "Content-Type": "application/json" }
  })
})

Edge Function 的安全红线: 由于 Edge Function 运行在受信任的服务端,你通常会在里面使用 service_role 密钥来绕过 RLS 强制更新打赏订单状态。但这绝不意味着你可以滥用它来逃避权限设计。 **原则是:**永远在 Edge Function 内部解析请求头里的用户 JWT,验证调用者的真实身份后,再做越权操作,把这扇“后门”守死。

通过合理建模、结合 Database 与 Edge 这两把尖刀,我们的协作系统后端骨架已经初具雏形。在下一篇文章中,我们将为这个系统插上视觉与交互的翅膀:处理设计手稿图片的上传(Storage),以及协作者之间的实时脑暴交流(Realtime)。