MoreRSS

site iconYasking | 东东修改

博主毕业后北漂两三年,2017 年初回到哈尔滨从事软件开发,2021 年初又回到北京。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Yasking | 东东的 RSS 预览

铁锅重生记

2025-08-30 22:36:03

家里有一个用了有两年的铁锅,媳妇儿炒菜炖菜都用它,前段时间一看,涂层已经面目全非。

可以买个新的,但一想,纯铁锅应该可以抢救下,有句话怎么说的 —— “新三年,旧三年,缝缝补补又三年”

注意:涂层锅不能重新开锅!涂层损坏后建议更换新锅

使用的是「老东北美食」大舅的教程:

【原来旧锅也能开锅养锅润锅,老师傅分享实用技巧,旧锅也能变新锅-哔哩哔哩】 https://b23.tv/4WJDMbz

全程耗时两小时,过程记录如下:

转着圈干烧铁锅,涂层肉眼可见的开始碳化。

烧了好一会儿后,用砂纸刷一刷锅内侧,不太建议用钢丝球(但我只有钢丝球,就用了钢丝球)

擦过后,有明显的铁锈痕迹,等会儿用水刷一下就好了,谁家锅底都有灰,刮刮刮刮刮...

而后用水刷锅内侧,可以放洗洁精,洗刷干净擦干后如下图

开火,上猪肥肉润锅!(两遍)

擦擦擦!

把过多的油倒掉,可以用水冲干净,先不要用洗洁精。

(这里缺失了再用食用油油润一遍锅的步骤,用锅铲抵着厨房用纸推着热油再把锅擦一遍)

倒掉油,清水刷锅后,铁锅重新开锅完成 ✅

养铁锅注意事项:

  1. 炒菜的锅只用来炒菜、不能用来炖煮;
  2. 及时刷锅,并擦干水分,如果不怕麻烦,用厨房用纸沾些油擦一遍;
  3. 刷锅时可以用洗洁精,但不能用钢丝球等坚硬物品清洁,会破坏涂层;

铁锅满血复活

技能 +1

成就感 +1

不锈钢盆与放心水源改造计划

2025-08-30 12:21:52

厨房用具更新

最近几个月,添置了一些 304、316 不锈钢材质的厨房用具,更早的缘由是前同事群里有人买了检测药水,发现家里很多的伪 304、316 不锈钢盆子。

我也好奇就买了药水检测,真发现了一批假冒伪劣产品,就计划着替换掉,经过一段时间挑选,购买了以下的 ”不锈钢们“

01.webp

先吐槽下「无印良品」的不锈钢盆子,价格真贵,但除了贵之外没别的毛病,银色的不锈钢小碟子可以在做饭时放葱姜蒜,小号的盆吃面条的时候用,最大号的用来备菜时泡一下蔬菜,沥水盆有些小了,后续应该会补个大号的

淘汰掉了家里不知道什么材质的 ‘不锈钢盆’,心情很棒

在「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

阅读莫言老师的第一本书是《不被大风吹倒》

不记得最开始听说莫言是什么时候,大概率是网友发的两只狗狗合影,神回复是“莫言和余华”,也可能因为莫言荣获了诺贝尔文学奖,很小的几率是因为《丰乳肥臀》这本标题有些“劲爆”的大作。

然后从史铁生的这段金句开始了解到余华和莫言,以及他们之间的友谊:

自从我腿残疾后, 家人们都很忌讳提起我的腿。 只有余华 他带我去踢球,让我守门。 他没把我当残疾人,也没把我当人。 —— 史铁生

《不被大风吹倒》这本书加到微信读书的书架有大几个月了,分几次才读完,最开始阅读时,其实我还没下定决心要开始阅读,只是在书店看到这本“畅销书”,好奇想看看写的什么,单从这本书来看,对莫言有了初步的了解—— 性格平和,善于观察、学习,铭记自己来自哪里,能往前看的人

这本书是多篇不同时期写作文章的合集,我觉得蛮有意思的是不同时期写的文章,组合到一起,风格是一致的,简单说就是 “有种质朴感的乡土情怀”。阅读时有一种轻松感,有很多片段,细想下藏着不少艰难,但作者巧妙的叙事,没有将担子放到读者肩上。

书中有作者儿时的回忆,有对阅读的热爱,有关于写作的心得,以及文学大家那里学到的东西,前几些天刚读过卡夫卡的《审判》,莫言老师书中也提到卡夫卡的的好几本书,说来也是很巧,感觉过一阵儿可以再选几本卡夫卡的小说读起来。

这本书很像是作者跟年轻大学生分享交流时的分享,我认为很适合作为阅读莫言老师书籍的入门之选。

写下这篇笔记的时候,我已经选择好了下一本书——《丰乳肥臀》,去看看莫言老师的“高密东北乡”,都诉说着怎样的故事。

基于 Supabase 构建示例应用(中篇):实现 Vue 前端页面

2025-08-29 18:55:12

构建 Vue 项目

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

引入 Supabase 依赖

安装库

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>

这里有一些容易混淆的地方需要注意

02.webp

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>

显示出来三篇已发布文章(标题相同)

导出表 Types

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

Vibe Coding 环节

好了,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...

06.webp

页面功能初步完成

07.webp

多语言也支持的良好

调整策略

因为上篇实验中,缺少部分策略,可以在 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

08.webp

Github Pages、Cloudflare Pages、Vercel 作为 Demo 放到哪里都足够,根据我的个人习惯,选择部署到 Cloudflare Pages,因为我有一个域名由 Cloudflare 管理,绑定自定义域名时可以纵享丝滑


先上传前端代码到 Github,我的仓库是:sincerefly/vuebase-posty

登录 Cloudflare 面板

09.webp

选择 Workers & Pages,点击创建

注意先切换到 Pages,然后再点击 Get started

11.webp

选择项目后下一步

选择 Vue Framawork,参数默认,应该还记得 .env.local 文件,将里面的 VITE_SUPABASE_URLVITE_SUPABASE_PUBLISHABLE_KEY 设置到此处环境变量

点击部署,稍后可以看到服务已部署

13.webp

服务地址:https://vuebase-posty.pages.dev

.pages.dev 是 Cloudflare 提供的域名,子域名是服务名,重复会追加随机字符。

14.webp

添加自定义域名(可选)

15.webp

由 CF 托管的域名,无需手动配置

16.webp

稍等片刻

17.webp

地址:https://posty.donx-done.xyz

配置 Supabase 服务 URL 地址

18.webp

配置完成后,到页面进行注册测试,当头两棒子

{"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

19.webp

改成 20 封邮件后,可以找临时邮箱进行注册验证

Supabase 注册 URL 自动登录逻辑

  1. 用户点击邮件中的确认链接(http://web-host/#access_token=xxx&refresh_token=xxx&type=recovery)
  2. Supabase 客户端自动检测 URL 参数(detectSessionInUrl: true),自动创建 session 并触发 onAuthStateChange 事件
  3. 认证状态监听器处理 (src/stores/auth.ts)
  4. 应用初始化 (src/App.vue)

我也没仔细看,因为全程氛围编程,没写几行前端代码

记在最后

Vibe Coding 一些心得,描述需求时要全面,但让其实现代码时要分步实现。Debug 时,让其添加 Console 日志,将问题日志提交给它,定位会更快、更准确

前端使用 Vue 开发,部署到了 Cloudflare:https://posty.donx-done.xyz

20.webp

前端代码仓库:sincerefly/vuebase-posty

本文阶段性的目标已达成,这篇想了想,定为「中篇」,Supabase 还有不少值得探索的功能,放到「下篇」学习记录。

基于 Supabase 构建示例应用(上篇):数据库与接口

2025-08-23 18:07:16

目标:实现一个文章发布 Web 应用(Demo),包含用户注册、编辑、发布文章的功能,同时有广场(展示所有已发布文章、所有用户可以查看)、并使用管理员进行维护,技术架构是 Supabase + Vue,Vue 部署在哪里还未计划,以此为契机学习了解 Supabase 服务

本篇学习和记录了如下内容

  1. 体验 Supabase Console 建表、执行 SQL
  2. 创建 RLS 行级安全策略
  3. 了解不同的 API Key 类型、创建 API Key、Curl 命令调用接口
  4. 了解 Supabase 的 Users 和业务表的 Users 表关联方式(通过触发器)
  5. 模拟用户注册、登陆、创建文章、查看广场文章的接口使用场景
  6. Edge Functions 配置一些封控策略(未测试)

设计表

用户表

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();

Supabase 建表

点击 SQL Editor 侧边栏,右侧输入框执行 SQL 语句,执行后来到 Table Editor,可以看到已经成功创建两张表

01.webp

但是上方都有 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 提供邮件邀请和手动创建的方式

02.webp

可以手动创建用户

也可以点击 “Send invitation” 发送邀请到邮箱。

获取免费的 SMPT Server(可选)

最开始我以为 Supabase 发送邮件需要自己提供服务,就在网上找了一个 Brevo 这个服务,有免费额度,注册过程很丝滑,记录如下,这步可以跳过,直接使用 Supabase 的邮件服务就好,也不用设置。

服务地址:Free SMTP Server | Deliver to the Inbox Every Time

免费套餐每天 300 封,测试使用足够了

04.webp

注册后点击右上角的组织,选择 “SMTP & API”

05.webp

可以看到 SMTP 服务的服务器地址、用户名密码

06.webp

还需要添加 Sender,否则发不出邮件

这里我起的应用名字是 “Reader Bot”,邮箱就是我的 Gmail 邮箱,需要真实的邮箱地址,稍后会发送验证码验证。

07.webp

点击添加

提示我们的免费邮箱可能大概率进入收件人的垃圾邮箱,建议绑定域名,因为是测试,当然是 Anyway

验证完成后,可以看到新的 Sender 添加成功

09.webp

此时 Brevo 提供的邮箱服务已可用

在 Supabase 配置自己的 Email SMPT 服务(可选)

如果你申请了邮箱,可以在 Authentication 模块进行配置

10.webp

填入 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 表中发送邮件邀请用户,就可以收到邮件

11.webp

内容如下

其中的邮件内容模板在 “Emails - Templates” 内进行配置,访问的链接在 “URL Configuration - Site URL” 进行配置,暂时先不用修改

初识 API & Keys

在 Project Settings 的 API Keys 页面可以创建项目 Key

13.webp

创建后有一个 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 可以

通过 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

14.webp

翻到 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 可以看到已经多了一条记录

15.webp

然后我发现 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

注册、登陆不再重复粘贴代码,控制台已经可以看到用户

16.webp

解决 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级(下起了小雨)" }'

解决了

17.webp

查看 “广场” 文章

以用户身份分页请求 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

18.webp

已更新,预期应该是可以查询到了,但是依然返回 [],到 Supabase 翻翻,发现还是 RLS 策略缺失的问题;

Supabase SQL 执行页面有个功能,可以选择作为哪个 ROLE 执行

19.webp

默认使用 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 可以配置函数,以下的限制借助边缘函数实现

21.webp

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: 科幻小说是一种以科学或未来技术为基础,结合合理想象来探讨人类、社会与宇宙关系的文学体裁。它强调科学逻辑的可能性,同时通过虚构情境反映现实问题与思想。