跳到主要内容

场景实践

本文给出几个最常见的 PG 模式云存储落地场景。每个场景都包含 Bucket 契约、路径设计、RLS 策略和 SDK 调用示例,便于直接复制后按业务调整。

用户头像

Bucket 契约

Bucket: avatars
用途:用户头像
路径模板:<uid>/avatar.png
读取权限:登录用户可读,也可按业务改成公开读
上传权限:仅本人可上传到自己的 <uid>/ 目录
覆盖权限:仅本人可覆盖自己的头像
删除权限:仅本人可删除自己的头像
限制:5 MB;image/png、image/jpeg、image/webp

创建 Bucket

INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'avatars',
'avatars',
false,
5 * 1024 * 1024,
ARRAY['image/png', 'image/jpeg', 'image/webp']
);

RLS 策略

CREATE POLICY avatars_select ON storage.objects
FOR SELECT TO authenticated
USING (bucket_id = 'avatars');

CREATE POLICY avatars_insert_own ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
);

CREATE POLICY avatars_update_own ON storage.objects
FOR UPDATE TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
)
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
);

CREATE POLICY avatars_delete_own ON storage.objects
FOR DELETE TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
);

SDK 调用

const bucket = app.storage.from('avatars');
const uid = loginState.user.id;

const { error } = await bucket.upload(`${uid}/avatar.png`, file, {
contentType: file.type || 'image/png',
upsert: true,
});

if (error) {
throw error;
}

const { data } = await bucket.createSignedUrl(`${uid}/avatar.png`, 600);
console.log(data.fullSignedURL);

用户私有文件

Bucket 契约

Bucket: private-files
用途:用户私有附件
路径模板:<uid>/<filename>
读取权限:仅本人可读
上传权限:仅本人可上传到自己的 <uid>/ 目录
删除权限:仅本人可删除
公开访问:否

RLS 策略

CREATE POLICY private_files_select ON storage.objects
FOR SELECT TO authenticated
USING (
bucket_id = 'private-files'
AND (storage.foldername(name))[1] = auth.uid()
);

CREATE POLICY private_files_insert ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'private-files'
AND (storage.foldername(name))[1] = auth.uid()
);

CREATE POLICY private_files_update ON storage.objects
FOR UPDATE TO authenticated
USING (
bucket_id = 'private-files'
AND (storage.foldername(name))[1] = auth.uid()
)
WITH CHECK (
bucket_id = 'private-files'
AND (storage.foldername(name))[1] = auth.uid()
);

CREATE POLICY private_files_delete ON storage.objects
FOR DELETE TO authenticated
USING (
bucket_id = 'private-files'
AND (storage.foldername(name))[1] = auth.uid()
);

SDK 调用

const bucket = app.storage.from('private-files');
const uid = loginState.user.id;

await bucket.upload(`${uid}/resume.pdf`, file, {
contentType: 'application/pdf',
});

const list = await bucket.list(uid, { limit: 20, withDelimiter: true });
console.log(list.data?.objects);

const signed = await bucket.createSignedUrl(`${uid}/resume.pdf`, 300);
console.log(signed.data?.fullSignedURL);

团队文件

Bucket 契约

Bucket: team-files
用途:团队协作附件
路径模板:<team_id>/<filename>
读取权限:团队成员可读
上传权限:团队成员可上传
删除权限:团队管理员可删除
关联业务表:public.team_members(team_id, user_id, role)

业务表

CREATE TABLE public.team_members (
team_id text NOT NULL,
user_id text NOT NULL,
role text NOT NULL DEFAULT 'member',
PRIMARY KEY (team_id, user_id)
);

GRANT SELECT ON public.team_members TO authenticated;

RLS 策略

CREATE POLICY team_files_select ON storage.objects
FOR SELECT TO authenticated
USING (
bucket_id = 'team-files'
AND EXISTS (
SELECT 1 FROM public.team_members tm
WHERE tm.team_id = (storage.foldername(storage.objects.name))[1]
AND tm.user_id = auth.uid()
)
);

CREATE POLICY team_files_insert ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'team-files'
AND EXISTS (
SELECT 1 FROM public.team_members tm
WHERE tm.team_id = (storage.foldername(name))[1]
AND tm.user_id = auth.uid()
)
);

CREATE POLICY team_files_delete_admin ON storage.objects
FOR DELETE TO authenticated
USING (
bucket_id = 'team-files'
AND EXISTS (
SELECT 1 FROM public.team_members tm
WHERE tm.team_id = (storage.foldername(storage.objects.name))[1]
AND tm.user_id = auth.uid()
AND tm.role = 'admin'
)
);

SDK 调用

const bucket = app.storage.from('team-files');
const teamId = 'team_001';

await bucket.upload(`${teamId}/design.pdf`, file, {
contentType: 'application/pdf',
});

const { data } = await bucket.list(teamId, {
limit: 50,
withDelimiter: true,
});

console.log(data.objects);

文章封面图

Bucket 契约

Bucket: article-assets
用途:文章封面和正文图片
路径模板:articles/<article_id>/<filename>
读取权限:公开文章所有人可读;私密文章仅作者可读
上传权限:文章作者可上传
关联业务表:public.articles(id, owner_id, is_public, cover_key)

RLS 策略

CREATE POLICY article_assets_select ON storage.objects
FOR SELECT TO anon, authenticated
USING (
bucket_id = 'article-assets'
AND EXISTS (
SELECT 1 FROM public.articles a
WHERE a.cover_key = storage.objects.name
AND (a.is_public OR a.owner_id = auth.uid())
)
);

上传策略可根据你的业务流程选择:如果封面必须先有文章记录,建议通过云函数或服务端接口上传;如果允许前端先上传临时图片,再绑定到文章,则建议使用临时目录和定时清理策略。

如何向 AI 描述场景

建议用以下格式描述需求:

我要实现:团队文件
Bucket: team-files
路径模板:<team_id>/<filename>
登录体系:PG auth,用户 ID 使用 auth.uid()
业务表:public.team_members(team_id, user_id, role)
权限:团队成员可上传和读取;只有 role = admin 可删除
SDK:Web 端使用 app.storage.from('team-files')
请生成:Bucket SQL、RLS Policy、上传/列表/删除示例,以及越权测试用例

这种结构化输入比自然语言长段描述更适合 AI 处理,也更容易审查生成结果是否安全。