2025-08-30 22:36:03
家里有一个用了有两年的铁锅,媳妇儿炒菜炖菜都用它,前段时间一看,涂层已经面目全非。
可以买个新的,但一想,纯铁锅应该可以抢救下,有句话怎么说的 —— “新三年,旧三年,缝缝补补又三年”
注意:涂层锅不能重新开锅!涂层损坏后建议更换新锅
使用的是「老东北美食」大舅的教程:
【原来旧锅也能开锅养锅润锅,老师傅分享实用技巧,旧锅也能变新锅-哔哩哔哩】 https://b23.tv/4WJDMbz
全程耗时两小时,过程记录如下:
转着圈干烧铁锅,涂层肉眼可见的开始碳化。
烧了好一会儿后,用砂纸刷一刷锅内侧,不太建议用钢丝球(但我只有钢丝球,就用了钢丝球)
擦过后,有明显的铁锈痕迹,等会儿用水刷一下就好了,谁家锅底都有灰,刮刮刮刮刮...
而后用水刷锅内侧,可以放洗洁精,洗刷干净擦干后如下图
开火,上猪肥肉润锅!(两遍)
擦擦擦!
把过多的油倒掉,可以用水冲干净,先不要用洗洁精。
(这里缺失了再用食用油油润一遍锅的步骤,用锅铲抵着厨房用纸推着热油再把锅擦一遍)
倒掉油,清水刷锅后,铁锅重新开锅完成 ✅
养铁锅注意事项:
铁锅满血复活
技能 +1
成就感 +1
2025-08-30 12:21:52
最近几个月,添置了一些 304、316 不锈钢材质的厨房用具,更早的缘由是前同事群里有人买了检测药水,发现家里很多的伪 304、316 不锈钢盆子。
我也好奇就买了药水检测,真发现了一批假冒伪劣产品,就计划着替换掉,经过一段时间挑选,购买了以下的 ”不锈钢们“
先吐槽下「无印良品」的不锈钢盆子,价格真贵,但除了贵之外没别的毛病,银色的不锈钢小碟子可以在做饭时放葱姜蒜,小号的盆吃面条的时候用,最大号的用来备菜时泡一下蔬菜,沥水盆有些小了,后续应该会补个大号的
淘汰掉了家里不知道什么材质的 ‘不锈钢盆’,心情很棒
在「XMAN 小满家外贸尾货」淘宝店买了几个厨房小物件,说是卖的外贸尾货微瑕品,但是瑕疵得仔细才能找见,性价比拉满,捣蒜器用料足,小油锅材质扎实,看着就是能用很久的那种
电饭煲内胆,最开始下单的「大良印记」家的不锈钢内胆,到家后煮粥,漏气喷的四处都是,客服说可能是尺寸误差,给退了货,下单了另一家也还是漏气,甚至电饭煲憋气后发出砰一声,我都想要换电饭煲了,客服说换个密封圈试试,他家也卖,换上后果然问题解决,最开始没往密封圈上想是因为小米原装内胆不会有这个问题,想来也是学到了
要说注意水质还得从 “直把杭州做汴州” 事件说起,网友们把擦脸巾套在水龙头上,放一段时间水,擦脸巾就黄的不行,本着好奇心,我也在洗手盆的龙头上做了为期一周测试,也是黄的不行,还掺杂着不明杂质。
之后两天我再直接用自来水刷牙时,就会有心理和生理上的双重不适,容易恶心干呕,连夜下单了一个水龙头净水器(32元),下图是使用三周后的样子,送了砂纸,滤芯可以打磨后重复使用。
又添置了一个过滤花洒(22.9 元)
最后是厨房净水器
观望了一段时间京东京造的净水器,价格便宜,换滤芯会贵一些,最后选择在淘宝买了通用净水器,考量主要是是京东净水器的定制滤芯换一次比较贵,所宣称 5 年使用寿命,我觉着不靠谱,消耗品还是要常换。
另一个次要因素是研究了一段时间通用净水器,感觉到目前纯水方案都很成熟,完全可以像装电脑一样也组装一台净水器。
但结合实际情况,还是先买一个半成品,后续再自己改造,这样比较折中,购入价格 1245 元,净水器是整个发过来的,需要自己接到厨下(自备扳手)
75G 同量净水器 + 压力桶方案,两三人的小家庭用起来感觉足够,面板显示 3 PPM,实际测试数值 30+
水源 | PPM |
---|---|
自来水 | 230 |
小区制水机 | 10 |
某矿物质桶装水水 | 23 |
怡宝纯净水 | 0 |
净水器 | 30+ |
(测量日期:2025-07-26) |
计划 PP 棉一季度一换、RO 膜两年一换、其余滤芯的半年一换,因为通量小,滤芯价格便宜,滤芯品牌丰俭由人。
水龙头是双水的,洗碗用净水、直饮用纯水,对我来说最大的改变就是不用到小区净水机打水,也不用隔几天就在网上下单怡宝纯净水,方便了太多
添置了不锈钢厨具、”放心水源改造计划“ 的实施,得到了媳妇儿的高度评价。
另外也更换了马桶盖、下水道防臭管,感觉出租房也可以适度折腾,“花小钱办大事”,很大程度提升了幸福指数。
2025-08-29 21:37:48
阅读莫言老师的第一本书是《不被大风吹倒》
不记得最开始听说莫言是什么时候,大概率是网友发的两只狗狗合影,神回复是“莫言和余华”,也可能因为莫言荣获了诺贝尔文学奖,很小的几率是因为《丰乳肥臀》这本标题有些“劲爆”的大作。
然后从史铁生的这段金句开始了解到余华和莫言,以及他们之间的友谊:
自从我腿残疾后, 家人们都很忌讳提起我的腿。 只有余华 他带我去踢球,让我守门。 他没把我当残疾人,也没把我当人。 —— 史铁生
《不被大风吹倒》这本书加到微信读书的书架有大几个月了,分几次才读完,最开始阅读时,其实我还没下定决心要开始阅读,只是在书店看到这本“畅销书”,好奇想看看写的什么,单从这本书来看,对莫言有了初步的了解—— 性格平和,善于观察、学习,铭记自己来自哪里,能往前看的人
这本书是多篇不同时期写作文章的合集,我觉得蛮有意思的是不同时期写的文章,组合到一起,风格是一致的,简单说就是 “有种质朴感的乡土情怀”。阅读时有一种轻松感,有很多片段,细想下藏着不少艰难,但作者巧妙的叙事,没有将担子放到读者肩上。
书中有作者儿时的回忆,有对阅读的热爱,有关于写作的心得,以及文学大家那里学到的东西,前几些天刚读过卡夫卡的《审判》,莫言老师书中也提到卡夫卡的的好几本书,说来也是很巧,感觉过一阵儿可以再选几本卡夫卡的小说读起来。
这本书很像是作者跟年轻大学生分享交流时的分享,我认为很适合作为阅读莫言老师书籍的入门之选。
写下这篇笔记的时候,我已经选择好了下一本书——《丰乳肥臀》,去看看莫言老师的“高密东北乡”,都诉说着怎样的故事。
2025-08-29 18:55:12
Supabase 服务的 Vue 构建官方文档:https://supabase.com/docs/guides/getting-started/quickstarts/vue
创建了一个 Github 仓库,用来存放 Vue 前端项目
克隆项目
git clone [email protected]:sincerefly/vuebase-posty.git
初始化 Vue 项目
cd vuebase-posty
npm create vue@latest .
选项
回车确认后的 Oxlint(试验阶段)和 rolldown-vite(试验阶段)都不选择,示例代码也不需要
运行三连
# 安装依赖
npm install
# 格式化代码
npm run format
# 启动
npm run dev
安装库
npm install @supabase/supabase-js
创建环境变量文件
touch .env.local
将服务地址和 Supabase Publishable Key 填入(注意替换为自己的地址和密钥)
VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
这里有一些容易混淆的地方需要注意
Supabase 教程页面上显示出用户的 Anon Key,看着需要使用这个 key 作为SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY
,那上篇中的 “sb_publishable_JToCFTxxxxxx” 又是什么,用哪个呢?
这个以
sb_publishable_
开头的密钥,实际上就是anon key
(匿名公钥),只是 Supabase 在不同时期使用了不同的命名格式。
结论就是用谁都行,sb_publishable_ 是旧的,JWT 格式的密钥是更新的格式
新建 src/lib/supabaseClient.js 文件
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
修改 src/App.vue 文件
根据我的表 Posts 做了相应调整
<script setup>
import { ref, onMounted } from 'vue'
import { supabase } from './lib/supabaseClient'
const posts = ref([])
async function getPosts() {
const { data } = await supabase.from('posts').select()
posts.value = data
}
onMounted(async () => {
await getPosts()
})
</script>
<template>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>
显示出来三篇已发布文章(标题相同)
https://supabase.com/docs/guides/api/rest/generating-types
如果网络抽风,使用本地代理 npm 设置如下 npm config set proxy socks5://127.0.0.1:7897 npm config set https-proxy socks5://127.0.0.1:7897
使用后清理 npm config delete proxy npm config delete https-proxy
# 安装命令行工具
npm i supabase@">=1.8.1" --save-dev
# 打开浏览器登录
npx supabase login
登录后,如果之前未执行过初始化,先在项目根目录运行
npx supabase init
按需选择,默认都是 N
Generate VS Code settings for Deno? [y/N]
Generate IntelliJ Settings for Deno? [y/N]
Finished supabase init.
获取数据库的类型定义
mkdir -p src/types
# 生成 Schema,注意替换 PROJECT_REF,就是服务器 API 地址子域名那串字符
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > src/types/database.types.tss
好了,Step by Step 到此,接下来开始氛围编程
这是一个 Vue 项目,后端是 Supabase 服务,请实现以下功能:
表结构如下:
CREATE TABLE users (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
username TEXT UNIQUE CHECK (char_length(username) >= 3),
email TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
content TEXT,
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
已经定义了策略,所有用户(匿名、登录)都可以获取到已发布的文章,即 published_at 字段不为空的记录;用户可以创建、修改自己的文章。
以上是服务端一些表结构,另外已经通过 supabase cli 导出了数据库字段类型的 Schema,在 src/types/database.types.tss
前端功能说明
1. 页面顶部有两栏,一个是“广场”,另一个是“我的”栏目,右上角有注册、登录功能,广场展示所有已发布的文章,我的栏目,如果未登录,通过页面上的文字提示请先登录,登录后展示所有用户的文章,登录后的右上角展示用户名,点击弹出下拉,有设置和登出、设置页面目前可以设置语言偏好;
2. 用户我的页面的文章,需要显示是否发布,可以根据单选项过滤全部、已发布、未发布三个状态,文章后方应该有编辑、发布的按钮,支持修改标题和内容;
3. 需要支持多语言,目前仅需要适配中文、英文两种语言,用户选择语言后,应该在浏览器本地进行缓存;
编码风格说明
1.首先,保持现代化、但不要使用过于花哨的颜色,简洁、小清新为主
2.代码实现应注意解耦合和封装,不要多个逻辑放到一个大文件中
3.API 接口和数据库操作需要符合 Supabase 的使用规范和习惯
正好打算试试 Trae,不过目前的智能程度,真是爱了... ⬇️
还是配置 Proxy,使用 Cursor
3 Hours Later...
页面功能初步完成
多语言也支持的良好
因为上篇实验中,缺少部分策略,可以在 Supabase 面板删除掉所有策略,重新创建本示例所需的策略
TODO 再核对下:
-- posts 表
alter policy "允许匿名和登录用户查看所有已发布文章"
on "public"."posts"
to anon, authenticated
using (
(published_at IS NOT NULL)
);
create policy "允许登录用户创建自己的文章"
on "public"."posts"
for insert
to authenticated
with check (
-- 确保用户只能插入自己的帖子
user_id = auth.uid()
);
create policy "允许登录用户删除自己的文章"
on "public"."posts"
as PERMISSIVE
for DELETE
to authenticated
using (
auth.uid() = user_id
);
create policy "允许登录用户查看自己所有文章" -- 包含未发布
on "public"."posts"
as PERMISSIVE
for SELECT
to authenticated
using (
auth.uid() = user_id
);
alter policy "允许登录用户更新自己的帖子"
on "public"."posts"
to authenticated
using (
(auth.uid() = user_id)
with check (
(auth.uid() = user_id)
);
alter policy "用户每天只能插入10篇文章"
on "public"."posts"
to authenticated
with check (
((auth.uid() = user_id) AND (( SELECT count(*) AS count FROM posts posts_1 WHERE ((posts_1.user_id = auth.uid()) AND (posts_1.created_at > (now() - '1 day'::interval)))) < 10))
);
-- users 表
create policy "允许用户查看自己的用户信息"
on "public"."users"
for select
to authenticated
using (
(select auth.uid()) = id
);
CREATE POLICY "允许用户更新自己的用户信息"
ON "public"."users"
FOR UPDATE
USING (auth.uid() = id);
通过 Cloudflare Page 部署
因为编译后是纯前端页面,所以可以托管到 Pages 服务,可选择性很多,优先国外,因为国内 Page 服务可持续性 be like
Github Pages、Cloudflare Pages、Vercel 作为 Demo 放到哪里都足够,根据我的个人习惯,选择部署到 Cloudflare Pages,因为我有一个域名由 Cloudflare 管理,绑定自定义域名时可以纵享丝滑
先上传前端代码到 Github,我的仓库是:sincerefly/vuebase-posty
登录 Cloudflare 面板
选择 Workers & Pages,点击创建
注意先切换到 Pages,然后再点击 Get started
选择项目后下一步
选择 Vue Framawork,参数默认,应该还记得 .env.local 文件,将里面的 VITE_SUPABASE_URL
和 VITE_SUPABASE_PUBLISHABLE_KEY
设置到此处环境变量
点击部署,稍后可以看到服务已部署
服务地址:https://vuebase-posty.pages.dev
.pages.dev 是 Cloudflare 提供的域名,子域名是服务名,重复会追加随机字符。
添加自定义域名(可选)
由 CF 托管的域名,无需手动配置
稍等片刻
地址:https://posty.donx-done.xyz
配置完成后,到页面进行注册测试,当头两棒子
{"code":"over_email_send_rate_limit","message":"For security purposes, you can only request this after 49 seconds."}
{"code":"over_email_send_rate_limit","message":"email rate limit exceeded"}
这是一个 Supabase 配置,位置在 Authentication 下的 Rate Limit
改成 20 封邮件后,可以找临时邮箱进行注册验证
我也没仔细看,因为全程氛围编程,没写几行前端代码
Vibe Coding 一些心得,描述需求时要全面,但让其实现代码时要分步实现。Debug 时,让其添加 Console 日志,将问题日志提交给它,定位会更快、更准确
前端使用 Vue 开发,部署到了 Cloudflare:https://posty.donx-done.xyz
前端代码仓库:sincerefly/vuebase-posty
本文阶段性的目标已达成,这篇想了想,定为「中篇」,Supabase 还有不少值得探索的功能,放到「下篇」学习记录。
2025-08-23 18:07:16
目标:实现一个文章发布 Web 应用(Demo),包含用户注册、编辑、发布文章的功能,同时有广场(展示所有已发布文章、所有用户可以查看)、并使用管理员进行维护,技术架构是 Supabase + Vue,Vue 部署在哪里还未计划,以此为契机学习了解 Supabase 服务
本篇学习和记录了如下内容
用户表
CREATE TABLE users (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
username TEXT UNIQUE CHECK (char_length(username) >= 3),
email TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
users
表通过 id
字段扩展了 Supabase 内置的auth.users
表
文章表
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
content TEXT,
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
users
表和 posts
表之间存在一对多的关系,即一个用户可以拥有多篇文章。
触发器
-- 首先创建一个函数来更新 updated_at 字段
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- 为 users 表创建触发器
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 为 posts 表创建触发器
CREATE TRIGGER update_posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
点击 SQL Editor 侧边栏,右侧输入框执行 SQL 语句,执行后来到 Table Editor,可以看到已经成功创建两张表
但是上方都有 Unrestricted
标注,没有开启 RLS 策略,可以点击页面上的黄色 “RLS disabled” 按钮,或是运行以下 SQL 语句均可开启。
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
启用行级策略后,我们可以设置一些策略,例如:
允许用户只能插入(INSERT)属于自己的文章:
CREATE POLICY "用户只能发表自己的文章"
ON posts
FOR INSERT
WITH CHECK (auth.uid() = user_id);
这样,用户尝试插入文章时,Supabase 会检查要插入的 user_id
是否等于当前登录用户的 ID
备注:策略也可以在 Authentication 模块的 Policies 子模块中,点击表后进行可视化创建。
Supabase 提供邮件邀请和手动创建的方式
可以手动创建用户
也可以点击 “Send invitation” 发送邀请到邮箱。
最开始我以为 Supabase 发送邮件需要自己提供服务,就在网上找了一个 Brevo 这个服务,有免费额度,注册过程很丝滑,记录如下,这步可以跳过,直接使用 Supabase 的邮件服务就好,也不用设置。
服务地址:Free SMTP Server | Deliver to the Inbox Every Time
免费套餐每天 300 封,测试使用足够了
注册后点击右上角的组织,选择 “SMTP & API”
可以看到 SMTP 服务的服务器地址、用户名密码
还需要添加 Sender,否则发不出邮件
这里我起的应用名字是 “Reader Bot”,邮箱就是我的 Gmail 邮箱,需要真实的邮箱地址,稍后会发送验证码验证。
点击添加
提示我们的免费邮箱可能大概率进入收件人的垃圾邮箱,建议绑定域名,因为是测试,当然是 Anyway
验证完成后,可以看到新的 Sender 添加成功
此时 Brevo 提供的邮箱服务已可用
如果你申请了邮箱,可以在 Authentication 模块进行配置
填入 Brevo 获取到的邮箱服务信息
Sender email:k********[email protected]
Sender name:Reader Bot
Host:smtp-relay.brevo.com
Port number:587
Username:[email protected]
Password:(YOUR-SMTP key value)
此时,再回到 Authentication 下的 Users 表中发送邮件邀请用户,就可以收到邮件
内容如下
其中的邮件内容模板在 “Emails - Templates” 内进行配置,访问的链接在 “URL Configuration - Site URL” 进行配置,暂时先不用修改
在 Project Settings 的 API Keys 页面可以创建项目 Key
创建后有一个 Publishable key 可以在浏览器使用,格外注意,需要搭配 RLS 策略使用,它是可以公开的
sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj
页面下方还可以看到一个名为 default 的 Server Key 用于服务端机器、或者 Function、Workers 等
点击页面左侧的 API Docs 来到 API 页面
这里有一个知识点:Supabase 通过原生集成的 PostgREST,为数据库提供开箱即用的 Auto API,使开发者无需部署后端即可安全地进行基础的 CRUD 操作。
API 文档很细致,API 分为 Client API 和 Server API,Clinet API 可以
以下命令的 apikey 就是刚生成的 Publishable key(测试时可以使用临时邮箱, e.g. TEMP MAIL)
curl -X POST 'https://iurlblwfhmulfdysyqdz.supabase.co/auth/v1/signup' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "123456"
}'
返回(已格式化)
{
"id": "b0fec5be-879d-4912-863e-b7495a8906b9",
"aud": "authenticated",
"role": "authenticated",
"email": "[email protected]",
"phone": "",
"confirmation_sent_at": "2025-08-23T07:19:42.840305897Z",
"app_metadata": {
"provider": "email",
"providers": [
"email"
]
},
"user_metadata": {
"email": "[email protected]",
"email_verified": false,
"phone_verified": false,
"sub": "b0fec5be-879d-4912-863e-b7495a8906b9"
},
"identities": [
{
"identity_id": "c9527fc9-13c9-4c85-a2c1-5b776663f22e",
"id": "b0fec5be-879d-4912-863e-b7495a8906b9",
"user_id": "b0fec5be-879d-4912-863e-b7495a8906b9",
"identity_data": {
"email": "[email protected]",
"email_verified": false,
"phone_verified": false,
"sub": "b0fec5be-879d-4912-863e-b7495a8906b9"
},
"provider": "email",
"last_sign_in_at": "2025-08-23T07:19:42.817329844Z",
"created_at": "2025-08-23T07:19:42.81738Z",
"updated_at": "2025-08-23T07:19:42.81738Z",
"email": "[email protected]"
}
],
"created_at": "2025-08-23T07:19:42.772681Z",
"updated_at": "2025-08-23T07:19:44.000369Z",
"is_anonymous": false
}
会收到 Supabase 注册邮件,目前地址会跳转到 localhost:3000 地址,我们的前端 Demo 还没开发部署(但是需要点击一下确认链接进行 Supabase 的用户激活)
此处仅体验使用 Publishable key 调用 Supabase 后端 API,注册用户后登陆:
curl -X POST 'https://iurlblwfhmulfdysyqdz.supabase.co/auth/v1/token?grant_type=password' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "123456"
}'
返回
{
"access_token": "eyJhbGaciOiJ...NULE72KZGcdu6-LfeUdSbp8",
"token_type": "bearer",
"expires_in": 3600,
"expires_at": 1755937882,
"refresh_token": "u4jw6puieja6",
"user": {
"id": "b0fec5be-879d-4912-863e-b7495a8906b9",
"aud": "authenticated",
"role": "authenticated",
"email": "[email protected]",
"email_confirmed_at": "2025-08-23T07:21:22.503059Z",
"phone": "",
"confirmation_sent_at": "2025-08-23T07:19:42.840305Z",
"confirmed_at": "2025-08-23T07:21:22.503059Z",
"last_sign_in_at": "2025-08-23T07:31:22.263515322Z",
"app_metadata": {
"provider": "email",
"providers": [
"email"
]
},
"user_metadata": {
"email": "[email protected]",
"email_verified": true,
"phone_verified": false,
"sub": "b0fec5be-879d-4912-863e-b7495a8906b9"
},
"identities": [
{
"identity_id": "c9527fc9-13c9-4c85-a2c1-5b776663f22e",
"id": "b0fec5be-879d-4912-863e-b7495a8906b9",
"user_id": "b0fec5be-879d-4912-863e-b7495a8906b9",
"identity_data": {
"email": "[email protected]",
"email_verified": true,
"phone_verified": false,
"sub": "b0fec5be-879d-4912-863e-b7495a8906b9"
},
"provider": "email",
"last_sign_in_at": "2025-08-23T07:19:42.817329Z",
"created_at": "2025-08-23T07:19:42.81738Z",
"updated_at": "2025-08-23T07:19:42.81738Z",
"email": "[email protected]"
}
],
"created_at": "2025-08-23T07:19:42.772681Z",
"updated_at": "2025-08-23T07:31:22.270377Z",
"is_anonymous": false
}
}
以上都是 GETTING STARTED 的内容,接下来可以看下业务表相关的 API
翻到 Insert 语句,“创建一篇文章”
curl -X POST 'https://iurlblwfhmulfdysyqdz.supabase.co/rest/v1/posts' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Authorization: Bearer eyJhbGaciOiJ...NULE72KZGcdu6-LfeUdSbp8" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d '{ "title": "你好,世界!", "content": "北京今日天气:22-29度 多云 西北风3级" }'
报错
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"posts\""}%
RLS 行级策略不允许,执行以下策略:
-- 用户只能插入自己的帖子
CREATE POLICY "允许认证用户插入帖子"
ON posts
FOR INSERT
TO authenticated
WITH CHECK (true);
-- 用户只能更新自己的帖子
CREATE POLICY "用户只能更新自己的帖子"
ON posts
AS PERMISSIVE
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
再次调用,没有返回任何内容,查看 Table Editor 可以看到已经多了一条记录
然后我发现 users 表也没有记录,即 Supabase 的 auth.users 和我的 public.users 表没有关联,同时 posts 表的 user_id 也是空;
分别解决这两个问题,对于 users 未同步,可以创建一个触发器
-- 创建函数
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.users (id, email, username)
VALUES (
NEW.id,
NEW.email,
COALESCE(NEW.raw_user_meta_data->>'username', SPLIT_PART(NEW.email, '@', 1))
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 创建触发器
CREATE OR REPLACE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
好了,再注册个用户试试!
用户:[email protected]
密码:123456
注册、登陆不再重复粘贴代码,控制台已经可以看到用户
解决 posts 的 user_id 为空的问题可以创建如下触发器
-- 启用 uuid 扩展(如果尚未启用)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 创建触发器函数
CREATE OR REPLACE FUNCTION public.set_post_user_id()
RETURNS TRIGGER AS $$
BEGIN
-- 从 JWT 中获取用户 ID 并设置
NEW.user_id = auth.uid();
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 创建触发器
CREATE OR REPLACE TRIGGER set_post_user_id_trigger
BEFORE INSERT ON posts
FOR EACH ROW
EXECUTE FUNCTION public.set_post_user_id();
使用新的用户 Token 创建文章
curl -X POST 'https://iurlblwfhmulfdysyqdz.supabase.co/rest/v1/posts' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Authorization: Bearer eyJhbGaciOiJ...YNGEpJM2MkFp0pj4Hj45RY-h9L06M4" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d '{ "title": "你好,世界!", "content": "北京今日天气:22-29度 多云 西北风3级(下起了小雨)" }'
解决了
查看 “广场” 文章
以用户身份分页请求 10 篇文章
curl 'https://iurlblwfhmulfdysyqdz.supabase.co/rest/v1/posts?published_at=not.is.null&select=*' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Authorization: Bearer eyJhbGaciOiJ...YNGEpJM2MkFp0pj4Hj45RY-h9L06M4" \
-H "Range: 0-9"
查询出来 []
空数组,因为文章最开始我们定义了一个 “只能查询(SELECT)到已发布的文章” 的策略
现在正好试试 Server Key 的管理员 Key 的威力,批量更新 published_at 字段为当前时间(Server Key 要藏好)
curl -X PATCH 'https://iurlblwfhmulfdysyqdz.supabase.co/rest/v1/posts?id=in.(3,4,5)' \
-H "apikey: sb_secret_Xr48DdK*************pmR1XA_xLeM45LH" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d '{ "published_at": "now()" }'
apikey 设置为 sb_secret_xxxx 管理员密钥,无需 Authorization Header
已更新,预期应该是可以查询到了,但是依然返回 []
,到 Supabase 翻翻,发现还是 RLS 策略缺失的问题;
Supabase SQL 执行页面有个功能,可以选择作为哪个 ROLE 执行
默认使用 postgres,也可以切换为匿名角色或是 authenticated role,选择后可以选择特定的用户,到表的 RLS policies 页面,添加策略
using 条件就是发布时间不为空,允许了匿名和已登陆用户查看;
[
{
"id": 3,
"user_id": null,
"title": "你好,世界!",
"content": "北京今日天气:22-29度 多云 西北风3级",
"published_at": "2025-08-23T08:38:44.481162+00:00",
"created_at": "2025-08-23T07:58:08.479927+00:00",
"updated_at": "2025-08-23T08:38:44.481162+00:00"
},
{
"id": 4,
"user_id": null,
"title": "你好,世界!",
"content": "北京今日天气:22-29度 多云 西北风3级(下起了小雨)",
"published_at": "2025-08-23T08:38:44.481162+00:00",
"created_at": "2025-08-23T08:18:26.565641+00:00",
"updated_at": "2025-08-23T08:38:44.481162+00:00"
},
{
"id": 5,
"user_id": "badab300-959f-42f7-ae75-99d74f937804",
"title": "你好,世界!",
"content": "北京今日天气:22-29度 多云 西北风3级(下起了小雨)",
"published_at": "2025-08-23T08:38:44.481162+00:00",
"created_at": "2025-08-23T08:25:13.986663+00:00",
"updated_at": "2025-08-23T08:38:44.481162+00:00"
}
]
“广场” 功能没问题,通过接口查询到了所有已发布的文章;
RLS 策略:用户每天最多发表 10 篇文章
CREATE POLICY "用户每天只能插入10篇文章"
ON posts
FOR INSERT
TO authenticated
WITH CHECK (
auth.uid() = user_id AND
-- 可以添加其他限制条件,比如每天最多10篇
(SELECT COUNT(*) FROM posts
WHERE user_id = auth.uid()
AND created_at > NOW() - INTERVAL '1 day') < 10
);
在 Edge Functions 可以配置函数,以下的限制借助边缘函数实现
rate-limiter(30 请求每分钟)
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
// 内存存储速率限制
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
Deno.serve(async (req: Request) => {
// 获取客户端IP
const clientIP = req.headers.get('x-forwarded-for') || 'unknown';
// 速率限制检查
const now = Date.now();
const limitData = rateLimitMap.get(clientIP) || { count: 0, resetTime: now + 60000 };
// 重置计数器(每分钟)
if (now > limitData.resetTime) {
limitData.count = 0;
limitData.resetTime = now + 60000;
}
// 检查限制(每分钟30次)
if (limitData.count >= 30) {
return new Response(
JSON.stringify({ error: 'Rate limit exceeded' }),
{ status: 429 }
);
}
// 增加计数
limitData.count++;
rateLimitMap.set(clientIP, limitData);
// 返回成功响应
return new Response(
JSON.stringify({
success: true,
method: req.method,
remaining: 30 - limitData.count
}),
{ headers: { 'Content-Type': 'application/json' } }
);
});
post-content-size(限制 content 字段 10kb 大小)
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
Deno.serve(async (req: Request) => {
try {
// 读取请求内容
const content = await req.text();
// 计算内容大小(字节数)
const contentSize = new TextEncoder().encode(content).length;
// 检查大小限制(10KB = 10240字节)
if (contentSize > 10240) {
return new Response(
JSON.stringify({
error: 'Content too large',
max_size: '10KB',
actual_size: `${(contentSize / 1024).toFixed(2)}KB`
}),
{ status: 413 }
);
}
// 内容大小合格
return new Response(
JSON.stringify({
success: true,
size: `${contentSize} bytes`,
size_kb: `${(contentSize / 1024).toFixed(2)}KB`
}),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Invalid request' }),
{ status: 400 }
);
}
});
reg-user-limiter(每天限制最多 100 人注册)
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "jsr:@supabase/supabase-js@2";
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
Deno.serve(async (req: Request) => {
try {
// 查询今日注册数量
const today = new Date().toISOString().split('T')[0];
const { count, error } = await supabase
.from('auth.users')
.select('*', { count: 'exact', head: true })
.gte('created_at', `${today}T00:00:00`)
.lte('created_at', `${today}T23:59:59`);
if (error) throw error;
// 检查是否超过限制
if (count >= 100) {
return new Response(
JSON.stringify({
error: 'Daily registration limit reached',
limit: 100,
today_count: count
}),
{ status: 429 }
);
}
// 允许注册
return new Response(
JSON.stringify({
allowed: true,
remaining: 100 - count,
today_count: count
}),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500 }
);
}
});
AI 提供的 Function 函数,我配置上,但没测试是否能正常工作 🤷
不知不觉记录了不少内容,后端目前先了解这些,已覆盖 Demo 所需功能,Web 前端的开发放到下篇文章进行记录。
下篇会根据 Supabase 的文档示例,使用 Vue 开发前端页面,也会调研部署在哪个免费服务比较好。
2025-08-20 20:24:00
三体可能是我阅读的第一本科幻小说
三部曲之一的地球往事通过片段化的故事连接,将现实与科幻柔和在一起,将‘上世纪六七十年代的社会运动’、现代环保等主题围绕着三体故事的主线逐一展开。
动荡年代的苦难,引人反思人性中的善与恶,对地球文明生态破坏的惋惜,以及对宇宙秩序的大胆想象。
书中人物都做着符合自己人生经历的抉择,叶文洁主动联系外星文明,能感受到她对人类秩序和道德的彻底失望,也在对“高级”文明的盲目崇拜中下了赌注,以至暴露太阳系坐标,引发不可逆的后果。她既是受害者,也是推动历史走向未知的关键人物。
第一部的叙事和铺垫很棒,大名鼎鼎的《三体》,我也算是 ‘已窥’ 一二了
科幻小说的定义
GPT: 科幻小说是一种以科学或未来技术为基础,结合合理想象来探讨人类、社会与宇宙关系的文学体裁。它强调科学逻辑的可能性,同时通过虚构情境反映现实问题与思想。