实战教程:电商小程序
阅读前建议先了解 PG 模式概述,明确 PostgreSQL、身份认证、云存储和权限模型的关系。
本文以一个电商小程序为完整实战场景,带你端到端完成 CloudBase PostgreSQL 数据库下的数据建模、权限设计、REST API 调用的完整链路。覆盖的能力:
- 多表建模与外键约束
- 双层权限(GRANT + RLS)的综合运用
- 不同角色(
anon/authenticated/service_role)的差异化访问 - PostgREST REST API 的完整使用(筛选、排序、分页、Prefer)
- 基于
is_active的软下架、基于 JWT 的下单身份绑定、"订单不可改"等典型业务规则
💡 读完本文后,你就可以把同样的思路套用到博客、私人笔记、SaaS、社交等各种应用中。
场景概述
构建一个类似 Shopify / 有赞 / 小程序商城 的电商应用,需要:
| 功能 | 说明 |
|---|---|
| 🏪 商品浏览 | 所有人(包括未登录用户)可浏览上架商品 |
| 🔒 下架商品隐藏 | is_active=false 的商品对普通用户不可见 |
| 🛒 买家下单 | 登录用户可创建订单,买家身份自动绑定 |
| 👁️ 订单私有 | 每个买家只能看到自己的订单 |
| 🚫 订单不可改 | 买家下单后不能修改或取消,只有管理员可变更状态 |
| 👨💼 管理员全权 | 管理员可管理商品、处理所有订单状态流转 |
数据模型
┌────────────────────────┐ ┌─────────────────────────┐
│ products │ │ orders │
├────────────────────────┤ ├─────────────────────────┤
│ id (serial PK) │◄──┐ │ id (serial PK) │
│ name (text) │ │ │ product_id (int FK) │───┘
│ description (text) │ │ │ buyer_id (text, JWT) │
│ price (numeric) │ └──┤ quantity (int) │
│ stock (int) │ │ total_price (numeric) │
│ category (text) │ │ status (text) │
│ is_active (bool) │ │ address (jsonb) │
│ created_at (timestamptz)│ │ created_at (timestamptz)│
└────────────────────────┘ └─────────────────────────┘
前置准备
- 已 创建 PG 模式的云开发环境
- 已拿到 Publishable Key 和 API Key(详见 PG:身份认证 - 凭证与 JWT)
- 环境 ID 记为
<envId>,例如pg-test-3gxmdbdb580ecfd1 - API 端点:
- REST:
https://<envId>.api.tcloudbasegateway.com/v1/rdb/rest/v1 - Auth:
https://<envId>.api.tcloudbasegateway.com/auth/v1
- REST:
SQL 执行顺序
以下 SQL 严格按以下顺序执行,否则会因依赖关系报错:
建表 (CREATE TABLE)
→ 启用 RLS (ALTER TABLE ... ENABLE ROW LEVEL SECURITY)
→ 授权 (GRANT)
→ 创建策略 (CREATE POLICY)
清理时需反序:先删 Policy → 再解除 RLS → 最后删表(DROP TABLE ... CASCADE 可自动清理 Policy)。
🔧 SQL 可通过控制台的 SQL 编辑器 直接逐条执行,也可通过云 API
ExecutePGSql自动化部署(部分 DDL 在 API 路径下需用DO $$ BEGIN EXECUTE '...'; END $$包装重试,详见 架构与权限模型 - ExecutePGSql)。
第一步:建表
-- ═══════════════════════════════════════════════════
-- 商品表
-- ═══════════════════════════════════════════════════
CREATE TABLE public.products (
id serial PRIMARY KEY,
name text NOT NULL,
description text,
price numeric(10,2) NOT NULL,
stock int DEFAULT 0,
category text,
is_active boolean DEFAULT true, -- true=上架, false=下架
created_at timestamptz DEFAULT now()
);
-- ═══════════════════════════════════════════════════
-- 订单表
-- ═══════════════════════════════════════════════════
CREATE TABLE public.orders (
id serial PRIMARY KEY,
product_id int NOT NULL REFERENCES public.products(id),
-- ⭐ buyer_id 自动从 JWT 中获取当前用户的 sub(auth.users.id 类型为 varchar(64))
buyer_id varchar(64) NOT NULL
DEFAULT (current_setting('request.jwt.claims', true)::json->>'sub'),
quantity int NOT NULL DEFAULT 1,
total_price numeric(10,2) NOT NULL,
status text NOT NULL DEFAULT 'pending', -- pending → paid → shipped → completed
address jsonb, -- 收货地址(结构化 JSON)
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE INDEX idx_orders_buyer_id ON public.orders(buyer_id);
关键设计:
buyer_id使用DEFAULT (current_setting('request.jwt.claims', true)::json->>'sub')自动从 JWT 中获取当前用户 ID,无需前端传入,杜绝了身份伪造的可能。
第二步:启用 RLS
ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
启用 RLS 后,如果没有任何 Policy,所有非
service_role用户都无法访问任何数据(默认拒绝)。继续 下一步。
第三步:授予表级权限(GRANT)
-- ═══════════════════════════════════════════════════
-- products(商品表)— 所有人可读,仅管理员可写
-- ═══════════════════════════════════════════════════
-- 匿名用户:只能查看
GRANT SELECT ON public.products TO anon;
-- 已认证用户:也只能查看(普通用户不能操作商品)
GRANT SELECT ON public.products TO authenticated;
-- 管理员:全 部权限(BYPASSRLS 自动绕过 RLS)
GRANT ALL ON public.products TO service_role;
GRANT USAGE, SELECT ON SEQUENCE public.products_id_seq TO service_role;
-- ═══════════════════════════════════════════════════
-- orders(订单表)— 买家可查可下单,不可改不可删
-- ═══════════════════════════════════════════════════
-- 已认证用户:可查看 + 可下单,但不能修改和删除
GRANT SELECT, INSERT ON public.orders TO authenticated;
GRANT USAGE, SELECT ON SEQUENCE public.orders_id_seq TO authenticated;
-- 管理员:全部权限
GRANT ALL ON public.orders TO service_role;
GRANT USAGE, SELECT ON SEQUENCE public.orders_id_seq TO service_role;
-- 匿名用户:不授予 orders 任何权限(未登录不能操作订单)
💡 使用
serial/bigserial主键时,必须同时授予对应 SEQUENCE 的USAGE权限,否则 INSERT 会因为无法获取下一个自增值而失败。
第四步:创建 RLS 策略(Policy)
-- ═══════════════════════════════════════════════════
-- products 的 RLS Policy
-- ═══════════════════════════════════════════════════
-- 所有人只能看到上架商品(is_active = true)
CREATE POLICY products_select
ON public.products
FOR SELECT
USING (is_active = true);
-- ⚠️ 不需要创建 INSERT/UPDATE/DELETE 的 Policy
-- 因为 anon 和 authenticated 在表级权限上就没有写权限
-- service_role 拥有 BYPASSRLS,自动绕过所有 Policy
-- ═══════════════════════════════════════════════════
-- orders 的 RLS Policy
-- ═══════════════════════════════════════════════════
-- SELECT: 买家只能看到自己的订单
CREATE POLICY orders_select
ON public.orders
FOR SELECT
TO authenticated
USING (
buyer_id = (current_setting('request.jwt.claims', true)::json->>'sub')
);
-- INSERT: 买家下单时,buyer_id 必须是自己
CREATE POLICY orders_insert
ON public.orders
FOR INSERT
TO authenticated
WITH CHECK (
buyer_id = (current_setting('request.jwt.claims', true)::json->>'sub')
);
-- ⚠️ 故意不给 authenticated 创建 UPDATE/DELETE Policy
-- 配合表级权限(也没授予 UPDATE/DELETE),实现"订单不可改"的业务规则
TO <role> 的语义RLS Policy 的 TO <role> 子句用于限定该策略只对指定角色生效:
- 写了
FOR SELECT TO authenticated USING(...)— 这条 Policy 只对authenticated角色起作用,对anon完全不存在 - 没写
TO子句(FOR SELECT USING(...))— 对所有角色都适用(除service_role因 BYPASSRLS 自动绕过)
对没有任何匹配 Policy 的 (角色, 操作) 组合,默认拒绝——这是 RLS 的核心特性,也是为什么"启用 RLS 但不写 Policy"会让所有非 service_role 请求被拒。
所以本例中 orders 表对 anon 没有 Policy + anon 在 GRANT 层也没权限 → 双重锁定,匿名用户绝对访问不到订单。
权限效果一览
商品表(products)
| 操作 | anon(游客) | authenticated(买家) | service_role(管理员) |
|---|---|---|---|
| SELECT(上架商品) | ✅ | ✅ | ✅ |
| SELECT(下架商品) | ❌ | ❌ | ✅ |
| INSERT | ❌ | ❌ | ✅ |
| UPDATE | ❌ | ❌ | ✅ |
| DELETE | ❌ | ❌ | ✅ |
订单表(orders)
| 操作 | anon(游客) | authenticated(自己的订单) | authenticated(他人的订单) | service_role(管理员) |
|---|---|---|---|---|
| SELECT | ❌ | ✅ | ❌ | ✅ |
| INSERT | ❌ | ✅ | ❌(不能冒充下单) | ✅ |
| UPDATE | ❌ | ❌ | ❌ | ✅ |
| DELETE | ❌ | ❌ | ❌ | ✅ |
REST API 实操
所有请求都通过 HTTP Header 传 Token:
Authorization: Bearer <Token>
Content-Type: application/json
Token 的来源取决于角色:
┌───────────────────────────────────────────────────────┐
│ 前端(小程序 / Web) │
│ │
│ 游客模式:Publishable Key 作为 Token │
│ → role=anon, 无 sub │
│ │
│ 登录模式:调用 /auth/v1/signin 获取 access_token │
│ → role=authenticated, 有 sub │
└───────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────┐
│ 后端(云函数 / 服务端) │
│ │
│ 管理模式:API Key 作为 Token │
│ → role=service_role, BYPASSRLS │
└───────────────────────────────────────────────────────┘
场景 1:商品浏览(游客)
游客无需登录,用 Publishable Key 即可浏览商品:
# 浏览所有上架商品(按价格降序)
GET /v1/rdb/rest/products?select=id,name,price,category&order=price.desc
Authorization: Bearer <Publishable Key>
返回:
[
{ "id": 1, "name": "iPhone 15 Pro", "price": "8999.00", "category": "电子产品" },
{ "id": 2, "name": "MacBook Air M3", "price": "9499.00", "category": "电子产品" }
]
⚠️ 下架商品(
is_active=false)不会出现在结果中 —— 这是 RLS 自动过滤的,前端无需处理。
# 按价格区间过滤
GET /v1/rdb/rest/products?select=name,price&price=gte.5000&price=lte.10000
# 按分类浏览(URL 中的中文可直接写或按 URL 编码 %E7%94%B5%E5%AD%90%E4%BA%A7%E5%93%81)
GET /v1/rdb/rest/products?select=name,price&category=eq.电子产品