在掌握了身份认证与行级安全(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)。