【实战进阶(3/4) 】数据库进阶、产品设计、Bug调试、部署上线、域名配置

2366 人学过

十二、数据库进阶:离开扣子后,怎么选数据库?

前言

💡
我们前面使用了扣子编程里集成的数据库,即便是离开了扣子编程、采用了TRAE/Cursor,也仍然在使用扣子编程给我们的数据库
有几个关键问题始终存在,我们在本节课「数据库进阶」里讲清楚:
数据库SQL是怎么回事?
不用扣子编程的数据库,还可以选用哪些数据库服务呢?
要管理哪些数据?以及怎么管理数据?
想象一下这些场景:
用户注册后,账号信息需要永久保存
博客文章发布后,需要随时查询和展示
电商网站的订单记录需要追溯和分析
社交应用的用户关系网络需要高效查询
如果把数据简单地存储在 JSON 文件或 TXT 文件中会发生什么?
Plain Text
❌ 文件存储的问题:
- 多个用户同时写入会造成数据冲突
- 查询效率极低(需要读取整个文件)
- 无法保证数据的一致性
- 难以处理复杂的数据关系
- 数据备份和恢复困难
这就是为什么我们需要数据库(Database)——一个专门为存储、查询和管理数据而设计的系统

两种数据库

1.
关系型数据库(SQL 数据库)
代表:PostgreSQL、MySQL、SQLite
特点:数据存储在表格中,有严格的结构(Schema)
适用场景:需要复杂查询、数据关系明确、数据一致性要求高
2.
非关系型数据库(NoSQL 数据库)
代表:MongoDB、Redis、Firebase
特点:数据存储更灵活,通常是文档或键值对形式
适用场景:数据结构多变、需要极高性能、分布式场景
我们将专注于关系型数据库特别是 PostgreSQL。
关系型数据库的逻辑有点类似于管理一份 Excel 表格——有列名、有行数据、不同的表之间可以互相关联。这种结构非常贴近我们习惯的思维方式,理解和使用起来也更简单。
而我们做的产品本身也是强关系的数据联结。用户有订单,订单有商品,商品有分类——这些东西是紧紧绑在一起而不是孤立存在的。关系型数据库就是为了处理这种"数据之间有明确关系"的场景而设计的,用它来做 SaaS 产品,也是更合适的选择。

PostgreSQL

PostgreSQL(常简称为 Postgres 或 PG)是世界上最先进的开源关系型数据库之一。
我们在扣子编程里使用的数据库,其实也是PostgreSQL,你早就在使用它了。
如下图所示
我们需要理解 PostgreSQL 的基本架构:
Plain Text
数据库服务器(Database Server)
  └─ 数据库(Database)
      └─ 表(Table)
          └─ 行(Row)/ 列(Column)
1.
数据库(Database)
一个 PostgreSQL 服务器可以包含多个数据库,每个数据库是一个独立的数据容器。
SQL
-- 例如,你的项目可能有:
my_app_development  -- 开发环境数据库
my_app_production   -- 生产环境数据库
2.
表(Table)
表是存储数据的基本单位,类似于 Excel 的工作表。每个表有:
列(Column):定义数据的类型和结构
行(Row):实际存储的数据记录
3.
Schema
Schema 是表的结构定义,规定了:
有哪些列
每列的数据类型
哪些是必填的
数据之间的关系

Neon:一个流行的数据库服务

现在,让我们把视野从"数据库是什么"转移到"如何在现代开发中使用数据库"。
前面提出的问题:脱离扣子编程后,还可以选用什么数据库服务呢?
答案就是Neon,它非常好用和流行。
Neon 是一个 Serverless PostgreSQL 服务,它让你更方便的使用数据库服务,完全托管的 Serverless PostgreSQL 平台,专为现代开发者和云原生应用设计。相比其他数据库服务商,Neon只专注于做PostgreSQL数据库,在专业度和性价比层面都更具有价值,也更适合我们现阶段的需求。
你在前面学到的所有 PostgreSQL 知识,在 Neon 上完全适用。Neon 不是"类似 PostgreSQL"的东西,它就是 PostgreSQL,只是以更现代的方式提供服务。
在实际的开发中,你需要先在Neon中创建项目:https://console.neon.tech/
填写数据库名称
获取链接方式
上面红框标识出来的,就是Neon给我们的数据库连接字符串。
和扣子编程给我们的,是不是长得很像?
对,只要是Postgres数据库,连接字符串结构都是一样的,只不过字符串里包含的用户名、密码、服务器等信息不同。
将数据库的连接方法,填入env文件中
在实际的开发场景中,你在Neon中创建的一个项目,至少需要有2个数据库,一个用作开发环境,一个用作生产环境。
比如你更改了某个数据结构,在开发环境的数据库中,尝试没有问题后,再将最新的数据结构,推送到线上环境的数据库中
现在让我们新建一个数据库项目,完成以下的内容

SQL 基础

1.
CREATE - 创建表
SQL
-- 创建一个用户表
CREATE TABLE users (
  id SERIAL PRIMARY KEY,           -- 自增主键
  email VARCHAR(255) NOT NULL,     -- 邮箱,不能为空
  username VARCHAR(50) UNIQUE,     -- 用户名,必须唯一
  created_at TIMESTAMP DEFAULT NOW() -- 创建时间,默认当前时间
);
关键概念:
PRIMARY KEY:主键,表中每行数据的唯一标识
SERIAL:自动递增的整数
NOT NULL:该字段不能为空
UNIQUE:该字段的值必须唯一
DEFAULT:默认值
来到数据表界面,看到刚刚创建的表格
2.
INSERT - 插入数据
SQL
-- 插入一条用户记录
INSERT INTO users (email, username)
VALUES ('user@example.com', 'john_doe');

-- 插入多条记录
INSERT INTO users (email, username)
VALUES 
  ('alice@example.com', 'alice'),
  ('bob@example.com', 'bob');
neon 实操
3.
SELECT - 查询数据
SQL
-- 查询所有用户
SELECT * FROM users;

-- 查询特定字段
SELECT email, username FROM users;

-- 条件查询
SELECT * FROM users WHERE username = 'alice';

-- 排序
SELECT * FROM users ORDER BY created_at DESC;

-- 限制数量
SELECT * FROM users LIMIT 10;
4.
UPDATE - 更新数据
SQL
-- 更新特定用户的邮箱
UPDATE users 
SET email = 'newemail@example.com' 
WHERE username = 'alice';
5.
DELETE - 删除数据
SQL
-- 删除特定用户
DELETE FROM users WHERE username = 'bob';

-- ⚠️ 危险操作:删除所有数据
DELETE FROM users;  -- 慎用!

常用数据类型

PostgreSQL 提供了丰富的数据类型:
SQL
-- 数字类型
INTEGER          -- 整数:-2147483648 到 2147483647
BIGINT           -- 大整数
SERIAL           -- 自增整数
DECIMAL(10,2)    -- 精确小数,适合金额

-- 文本类型
VARCHAR(n)       -- 可变长度字符串,最大 n 字符
TEXT             -- 无限长度文本

-- 日期时间
DATE             -- 日期:2024-01-15
TIME             -- 时间:14:30:00
TIMESTAMP        -- 日期+时间:2024-01-15 14:30:00

-- 布尔类型
BOOLEAN          -- true/false

-- JSON 类型(PostgreSQL 特色)
JSON             -- JSON 格式数据
JSONB            -- 二进制 JSON,查询更快

需要存储的数据内容

了解了常用的数据类型,我们可以继续看看在构造数据库的时候也需要关注产品应该存储哪些数据。这个不需要我们自己去设计,直接交给 AI 就可以。
我们之前学写的 SPEC 里已经覆盖了对产品相对完整的需求,这时候我们只需要把这份 SPEC 发送给任何一个 AI 工具,让他帮我们分析这个产品需要在数据库里存储哪些数据,同时设计数据列表就可以了。我用的是下面这个提示词:
Plain Text
这是我要做的纸片人男友产品的SPEC,现在我要建立一个数据库,请你帮我设计一下数据库需要存储的数据内容和列表
我把虚拟男友的 SPEC 发给了大模型,就得到了下面这样一份非常完整的数据存储清单

数据库设计

好的数据库设计是应用成功的基础。这里是一些核心原则:
1.
主键设计
每个表都应该有主键,用于唯一标识每一行数据。
就像每个人都有一个唯一的身份证号,不管叫"张伟"的人有多少个,身份证号永远只属于一个人——主键在数据库里扮演的就是这个唯一标识的角色。
SQL
-- 方案 1:自增整数(最常见)
CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL
);

-- 方案 2:UUID(分布式系统常用)
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL
);
选择建议:
小型应用:SERIAL 足够
大型分布式系统:考虑 UUID
2.
数据关系
现实世界中的数据往往存在关联,数据库通过外键(Foreign Key)来表达这些关系。
一对多关系
一个用户可以写多篇文章:
SQL
-- 用户表
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50) NOT NULL
);

-- 文章表
CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  content TEXT,
  author_id INTEGER REFERENCES users(id), -- 外键
  created_at TIMESTAMP DEFAULT NOW()
);
多对多关系
一个学生可以选修多门课程,一门课程可以有多个学生:
SQL
-- 学生表
CREATE TABLE students (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);

-- 课程表
CREATE TABLE courses (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);

-- 中间表(关联表)
CREATE TABLE enrollments (
  student_id INTEGER REFERENCES students(id),
  course_id INTEGER REFERENCES courses(id),
  enrolled_at TIMESTAMP DEFAULT NOW(),
  PRIMARY KEY (student_id, course_id)  -- 复合主键
);
3.
索引:提升查询性能
索引就像书的目录,帮助数据库快速找到数据。
SQL
-- 为常用查询字段添加索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author ON posts(author_id);
何时使用索引:
✅ WHERE 子句中频繁使用的字段
✅ JOIN 操作中的关联字段
✅ ORDER BY 排序的字段
❌ 很少查询的字段(索引也占用存储空间)
4.
约束:保证数据质量
约束是数据库层面的数据验证规则。
SQL
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR(200) NOT NULL,
  price DECIMAL(10,2) CHECK (price > 0),  -- 价格必须大于 0
  stock INTEGER DEFAULT 0 CHECK (stock >= 0), -- 库存不能为负
  category VARCHAR(50) NOT NULL,
  
  -- 组合唯一约束
  UNIQUE (name, category)
);

Migration

当你在 Git 中管理代码时,你可以追踪每一次修改。但数据库的结构(Schema)呢?每次修改项目,可能都会对数据表的字段进行更改
Migration(数据库迁移) 就是数据库的版本控制系统,类比为数据库的专用 git
想象一个真实场景:
Plain Text
第 1 周:你创建了 users 表,只有 email 和 password
第 3 周:产品经理要求添加 username 字段
第 5 周:需要添加 profile_image 字段
第 8 周:发现 email 字段长度不够,需要扩展
如果没有 Migration:
❌ 每个开发者手动修改数据库,容易遗漏或出错
❌ 生产环境和开发环境的表结构不同步
❌ 无法回滚到之前的版本
❌ 新同事加入项目时,不知道如何初始化数据库
有了 Migration:
✅ 每次修改都记录在代码中
✅ 可以像 Git 一样追踪每次变更
✅ 可以在任何环境重放这些变更
✅ 支持回滚(Rollback)
Plain Text
migrations/
  ├── 001_create_users.sql          # 第一次迁移:创建用户表
  ├── 002_add_username.sql          # 第二次迁移:添加 username 字段
  ├── 003_create_posts.sql          # 第三次迁移:创建文章表
  └── 004_add_posts_published.sql   # 第四次迁移:添加发布状态字段

其他主流的数据库服务商

Neon是最好用的数据库服务商之一,如果只推荐一个,我就推荐它了。
当然,这个世界上还存在其他也很好用的,它们各有各的优点,也值得你了解。
Supabase
💡
一句话总结: Supabase除了提供Postgresql数据库服务,还提供用户系统、Serverless API等全栈开发者需要用到的很多好东西!
我们2025年的两期课程,其实是用Supabase为载体来讲的,到了2026年的第三期开始,才放弃了它。
我下面大概介绍一下,有余力的同学可以顺着我给出的介绍、用AI浏览器打开它的官网放在旁边详细研究。
Supabase 是一个开源的 Firebase 替代品。它自称为“The Open Source Firebase Alternative”,旨在为开发者提供构建应用程序后端所需的一系列工具,但建立在开源技术之上,给予开发者更大的灵活性和控制权。
想象一下,你需要为你的应用程序(无论是网页应用还是移动应用)开发后端功能,例如用户认证、数据存储、文件存储等。传统上,你需要自己搭建服务器、配置数据库、编写大量的后端代码。Firebase 的出现简化了这个流程,提供了一整套“后端即服务”(Backend-as-a-Service, BaaS)。而 Supabase 则以开源的方式提供了与 Firebase 类似的核心功能,甚至更多。
它的核心理念是使用一系列企业级的开源工具,并将它们整合在一起,提供一个统一、易于使用的产品。
Supabase 将多个强大的开源项目打包成一个无缝的服务,其主要功能包括:
1.
数据库 (Database):
2.
用户认证 (Authentication):
3.
对象存储 (Storage):
4.
无服务器函数 (Edge Functions):
5.
自动生成 API (Auto-generated APIs):
为啥我推荐 Supabase?就一个字,爽!
起步门槛低到离谱:你敢信?3 分钟就能创建一个生产级数据库!我第一次接触时简直惊呆了,比买个淘宝账号还简单,用户少的时候,还不要钱!
全家桶服务:数据库、身份验证(包括登录、注册、社交媒体登录)、存储、实时功能、邮件功能,全都有!别问,问就是"一把梭",省心!
不付钱也能白嫖很久:初期用户少,完全够用。等产品赚钱了,再用它的付费套餐,也不贵;等用户特别多了,还可以考虑自己租用服务器来部署 Supabase 的开源版本,无缝迁移。
推荐一些官方教程
Supabase 官方入门指南(Next.js 版)
Supabase 官方示例代码和教程(Next.js 版)
主流云平台
主流云平台,无论是Amazon AWS云、阿里云、腾讯云、火山云,也都直接提供数据库服务。
直接使用云平台的数据库,优点是稳定、功能更完整,缺点比起Neon/Supabase来说,操作稍微麻烦一点。
以腾讯云为例(腾讯云相对比较便宜)
打开官网,在它产品列表里找到「数据库」分类,一定能够找到“PostgreSQL数据库服务”。
火山云也一样,云厂商的产品布局,都是类似的
我们可以按需采购。
无论连接哪种数据库服务,我们都只需要关注连接字符串即可。

管理产品数据的方式

Vibe Coding 一个管理后台
管理后台一般只做常用数据的管理和展示,比如用户资料、订单信息、项目状态等,它的展示更直观,体感也更好。
你可以直接在当前产品项目做管理后台,这是我直接在虚拟男友的项目里发送使用的提示词:
Markdown
你是资深全栈工程师。请先审查当前项目,然后在“现有项目”中实现一个相对完整的管理后台。不要脱离当前技术栈,不要重造数据库、认证或 API,优先复用现有结构。

核心原则:
1. 先扫描项目代码,识别当前技术栈、路由方式、UI 组件库、认证方式、数据库访问层、现有 API/Server Actions/ORM。
2. 优先复用现有页面结构、样式体系、数据库 schema、接口封装、鉴权逻辑。
3. 不要重新造一套后端;如果缺少必要接口,只补“最小必要”的管理接口。
4. 不要新增删除功能。
5. 如果现有表字段名称与我下面写的不完全一致,请基于真实 schema 做合理映射,并在最终说明里告诉我“字段映射关系”。
6. 所有管理功能都应限制为管理员可访问;如果项目里已有角色权限系统,直接复用;如果没有,请用当前认证体系补一个最小可用的 `isAdmin` 保护,并清楚标注 TODO。
7. 页面风格简洁、清晰、偏后台风格,优先可用性,不要做花哨视觉。
8. 先给出实施计划,再开始改代码。改完后告诉我新增/修改了哪些文件、路由和数据接口。

我要的后台范围:

一、后台路由结构
- `/admin`
- `/admin/users`
- `/admin/orders`

二、后台通用能力
请实现一个统一的后台布局,包含:
- 左侧导航:概览、用户管理、订单管理
- 顶部标题区
- 登录态/管理员身份校验
- 加载态、空状态、错误提示
- 简洁表格风格
- 基础分页(如果项目已有分页方案就复用,没有就做最简单可用版)
- 搜索和筛选区域
- 响应式不必做到复杂,但桌面端体验要清晰稳定

三、`/admin` 概览页
请做一个最小可用的 dashboard,总览常见运营信息。如果真实 schema 支持,就展示:
- 用户总数
- 最近新增用户数
- 订单总数
- 最近订单数
- 如果订单表里有金额字段,再展示总成交额或最近成交额
如果某些指标没有对应字段,就跳过,不要硬造假数据。

四、`/admin/users` 用户管理页
目标:
1. 展示用户列表
2. 默认展示字段:`id`、`name`、`email`、`created_at`、`status`
3. 支持按 `name` 和 `email` 搜索
4. 支持按 `status` 筛选
5. 支持分页
6. 支持编辑 `status`
7. 不要删除功能

具体要求:
- 列表使用表格展示
- 搜索框可以搜索 name/email
- 如果 schema 里 `name` 不存在,可用最接近的字段替代,如 `nickname`、`full_name`
- 编辑方式可以是弹窗、抽屉或行内编辑,但要简洁稳定
- 更新成功后刷新列表,失败时给出明确错误提示
- 如果存在用户详情页或合适的查看方式,可以增加“查看详情”,但不是必须
- 不要允许编辑敏感字段,先只改 `status`

五、`/admin/orders` 订单管理页
目标:
1. 展示订单列表
2. 优先展示字段:`id` / `order_no`、`user`、`amount`、`status`、`created_at`
3. 支持搜索订单号、用户姓名、用户邮箱(以真实 schema 能支持的为准)
4. 支持按订单状态筛选
5. 支持查看订单详情
6. 支持编辑订单 `status`
7. 不要删除功能

具体要求:
- 如果订单与用户有关联,请把用户信息通过关联查出来并展示
- 如果没有 `order_no`,可直接展示 `id`
- 如果没有 `amount`,不要硬加金额逻辑
- 订单详情里优先展示:订单基本信息、关联用户、创建时间、状态、金额、备注等真实存在的字段
- 编辑订单状态时,优先复用现有状态枚举;如果项目中已有状态流转约束,请遵守现有逻辑

六、数据访问与实现要求
- 优先使用服务端获取数据,不要把数据库密钥暴露到客户端
- 如果项目已有 API 层,就复用 API;如果项目是服务端直连数据库,也请沿用现有模式
- 查询、搜索、筛选、分页都尽量复用现有查询封装
- 不要复制一套新的 types/schema;优先复用已有类型定义
- 如果用户表、订单表已经存在关联关系,请正确使用关联查询
- 如果数据库字段命名不一致,请按实际结构实现,不要为了迎合提示词而硬改数据库

七、交付方式
请按以下顺序执行:
1. 先总结当前项目技术栈与可复用结构
2. 给出简洁的实施计划
3. 开始实现后台页面和必要接口
4. 完成后说明:
   - 新增/修改了哪些页面和文件
   - 复用了哪些现有结构
   - 用户和订单模块各自支持了哪些功能
   - 哪些地方因为真实 schema 限制做了适配
   - 下一步如果我要继续做 products、payments、logs,应该如何沿用这套后台结构
于是你会得到这样一个配置登录的管理后台。
在管理后台里能看到所有用户数据、订单记录,甚至可以直接修改数据,所以必须加上登录验证——只有你自己(或者你授权的人)才能进来
登陆后就可以看到产品的用户、订单、使用状况等具体信息,你可以根据需要再增加需要展示、管理的数据内容。
通过 TablePlus 等数据库管理工具直接操作
TablePlus下载链接:https://tableplus.com/
和管理后台在应用界面管理数据的方式相比,这种方式是在数据库层直接管理数据。
从欢迎屏幕,单击Create a New Connection,选择数据库类型,我们这里使用 PostgreSQL。
点击下方的Import from URL,直接把之前我们的数据库连接字符串,也就是长的像postgresql://postgres¥%#……&这样的字符串粘贴进来连接到我们的演示 PostgreSQL 数据库
跳转到管理页面,可以使用⌘ + P/ Ctrl + P或点击右上角的搜索按钮打开任何内容,包括数据库、表、模式、视图、函数……
在表格视图中,你可以通过双击数据单元格或使用右侧边栏中的切换视图来直接编辑数据
需要注意的是,TablePlus 不会自动将修改数据库的查询发送到服务器,因此,在应用程序中查询和编辑数据后,需要将更改提交到数据库。有两种方法可以提交更改:
使用左上角操作控件上的提交按钮。
使用快捷键:⌘ + S/Ctrl + S

作业

1.
尝试Neon数据库,完成课程内容
2.
理解数据库的核心概念、理解简单的SQL语句
3.
尝试把你的产品从「使用扣子编程提供的数据库」切换为「使用Neon(或任意另一家服务商)提供的数据库」

十三、如何迅速提升产品设计感

标准的SaaS网站首页都同一个结构

前面的课程里,你已经通过Toolify、TrustMRR看过了很多AI产品。
你有没有发现,它们的首页基本上都是同一个结构 ——
我画了个图,你带着这个图再去看几个产品
除了主页、定价页和注册页面外,一个专业的 SaaS 网站还需要包含以下几个重要页面,它们虽然不直接产生转化,但对建立信任和满足法律要求至关重要:
1.
服务条款(Terms of Service)页面
作用:明确用户与平台之间的权利和义务关系
内容建议
使用简明易懂的语言描述双方责任
说明服务使用限制和禁止行为
解释账户终止条件和争议解决方式
2.
隐私政策(Privacy Policy)页面
作用:说明如何收集、使用和保护用户数据,符合各地数据保护法规
内容建议
列出收集的数据类型及用途
说明用户的数据控制权(查看、修改、删除等)
解释数据共享政策和安全措施
3.
退款政策页面
作用:清晰说明如何处理用户退款请求,增加购买信心
内容建议
详细说明退款条件和时间限制(如"14 天无理由退款")
解释退款流程和所需步骤
提供常见问题解答,减少客服压力
4.
关于我们(About)页面
作用:展示公司背景和团队,建立信任感
内容建议
分享创立故事和企业使命
介绍核心团队成员和专业背景
展示成就和里程碑(如用户数量、获得的投资等)
5.
联系我们(Contact)页面
作用:提供多种联系方式,增强客户支持体验
内容建议
设置简单的联系表单
提供客服邮箱和工作时间
考虑添加实时聊天支持
若有实体办公室,可提供地址和地图
6.
博客(Blog)页面
作用:分享行业知识,提高 SEO 排名和用户粘性
内容建议
发布与产品相关的教程和最佳实践
分享行业趋势和见解
定期更新保持活跃度
这些页面共同构成了一个完整、专业的 SaaS 网站结构,不仅满足法律要求,还能有效建立品牌信任和提升用户体验。
对于刚起步的 SaaS 产品,建议优先完成法律必需的页面(服务条款、隐私政策),再逐步完善其他内容。
既然都是同一个结构,那么,就很好办了。
这个世界上有大量的“积木”(组件库),可以迅速帮助我们的网站提升设计感。

通过第三方组件库迅速提升设计感

为了让我们的网站更专业更漂亮,我们可以快速地去参考别人写好的一些漂亮主题,添加到我们的网站里面。
打开下面的组件库,选择喜欢的组件(components)
在这个教程中,我们以第一套组件库 21st.dev 为例。
第一步,去 21st.dev 找组件
比如,我某天刷到的这个鼠标跟随发光边框效果,觉得这个不错,可以直接复刻过来
第二步,复制组件代码
点击 Copy Code 或者同时复制Installation+下方的一整坨代码块,粘贴到 TRAE ,告诉它"把这个效果应用到XXX区域。"
第三步,等待运行
稍等运行,组件的效果就复刻成功了
再回看一下,感觉现在的首屏有点太平淡了,我想增加一个首屏动态效果,找到这个:
一样的操作,把组件代码丢给 TRAE,让它更换。
运行过程中可能会出现报错,不用着急,直接告诉 TRAE 出现的问题即可
比如我发现文字的出现效果和组件里的不一样,而我就想要组件里的
再耐心等待不到5分钟,就改好了
我又发现这个按钮和界面很匹配,又更有设计感,也可以替换一下
按钮的替换也就完成了

追求品质

你的产品体验,足够好吗?目标用户怎么说?
你的界面设计,足够专业吗?这个年头,就算相亲,也要先看脸。如果脸不行,人家都不一定有耐心去了解你的内在美。 虽然你是草台班子,但是不能让用户轻易看出来你是。
提升审美:全覆盖级阅读控件库,让它们存在于你的潜意识里。当你设计产品的时候,潜意识中自然会浮现“这个功能区域,可以借鉴我曾经在哪里看到过的哪个形式”
以下是我们第一期的学员@李江 在学习完本课后,完成的作品。请注意它的审美和品质。所使用的方案,全在本课内。
但你有没有发现,光知道"要提升审美",还不够。
审美怎么提升?
靠看。靠刷。靠见多识广。
你见过的产品越多,刷过的组件越多,你脑子里的"素材库"就越厚。
等你真正要做产品设计的时候,潜意识会自动跳出来:
"这个地方,我好像在哪儿见过一个很好看的方案。"
这个感觉,是刷出来的。不是想出来的。
没事多逛逛 21st.dev,多看看别人的产品首页,多翻翻 Product Hunt。
让那些好设计,住进你的潜意识里。

一个小彩蛋

曾经,我的Raphael AI 因为一个好看的特效鼠标组件,在Twitter上获得了十几万的自传播流量。
你能找到,我用的是哪个组件吗? (提示:就在上面我推荐的一大堆组件库当中 😄 )

有没有更快的方式?

有的。再传授一招:先大后小,分而治之。
1.
找一个美观又简单、符合你产品调性的 SaaS 网站。我们不妨假设你找到的是 https://cleanup.pictures/
2.
打开任何一个AI编程工具, 在输入框里告诉它:
Plain Text
请用Next.js/Shadcn/TailwindCSS框架复刻 https://cleanup.pictures/ 
按照Section/Component,尽量拆分成为更细的Component并且放到合适的目录

{如果你再贴上cleanup.pictures的截图,就更好了}
注意事项
1.
表达的时候尽量更清楚一点。如果你的产品已经有很多功能了,这时候你复制一个和你功能完全不搭配的布局进去,不太可能取得很好的效果。
2.
尽量一小一块一小块的复制,精确性更强。 如果整页克隆,有更大的不确定性
我懒,还想要再快一点
💡
一句话:用你喜欢的方式(如TRAE或者扣子编程)做原型 → 购买漂亮的模板、再用claude code一键穿衣服
你可以购买一些好看的模板,直接让AI换上。
提示:这个任务对于Cursor/TRAE来说难度比较大,不容易一口气做好。
但是Claude Code和Codex的最强模型,可以一次性做到。
对于一个我用V0(类似扣子编程)做的项目,它本来长这样:
我购买了一个模板,让claude code帮我“一键穿衣服”
5分钟后,直接变成这样。
我穷,我买不起模板
..好吧,服了。
还有两招
1.
用Stitch出设计稿,再套进项目里
Stitch是 Google Labs 的一项新实验,能够将简单的提示和图像输入转换为复杂的 UI 设计和前端代码。
不用 Figma。不用设计师。不用学任何设计工具。
在创建设计之前,可以在界面中直接选择 Web 和 APP 布局模式。
这里我先选择 APP 模式,输入以下提示词
Plain Text
使用pencil mcp
在当前活跃的画布上
然后重新设计「QQ音乐」手机App的所有主要界面,界面要像苹果公司的设计风格,浅色系
初版的简单设计就完成了,之后可以根据需要去做调整。
背景颜色太单调?我可以让它直接改背景颜色
首页需要微调?直接选中首页,对话框会直接添加作为上下文,然后直接说你要改什么
它会给你两个版本对比——改之前和改之后,根据需要选择或再次修改就可以了
你也可以手动修改,选择 Modify 的 Edit 模式,点击你想要修改的组件,直接修改
改完后点击右上角的 Export,可以导入Figma,也可以通过 .zip/Code to Clipboard 下载代码
这里我选择的是.zip,下载下来后大概是这样的形式
用 TRAE 打开整个 stitch_now_playing 文件夹,再把下面这段代码发给 TRAE
Plain Text
请根据我提供的 Stitch 导出文件,还原成一个可运行的 React 项目。

要求:
1. 包含 4 个页面:discover、now_playing、profile、search
2. 使用 React + Tailwind CSS
3. 把底部导航抽成公共组件
4. 把卡片、列表项、播放器控件等抽成可复用组件
5. 页面风格尽量和 Stitch 导出的 UI 保持一致
6. 先实现静态 UI,再补充页面切换
7. 每个页面参考对应文件夹中的 code.html 和 screen.png
等它运行好,就迁移成功了!接下来就可以在 TRAE 里继续打磨替换组件,增加功能等等
2.
用 Pencil MCP,在 TRAE 等IDE编程工具里直接出图
在 TRAE 的扩展商店里检索“Pencil”并安装
安装完成后下方会出现一个铅笔的图标,它就是Pencil了
拓展安装好后,还需要手动添加配置,点击页面右上角的设置—选择MCP
选择添加—手动添加—再使用下面这段JSON文件修改好相关信息,复制进去就可以了
JSON
{
  "mcpServers": {
    "pencil": {
      "command": "/Users/<用户名>/.trae-cn/extensions/highagency.pencildev-<版本号>/out/mcp-server-darwin-arm64",
      "args": ["--app", "trae_cn"]
    }
  }
}
点击左上角的New .pen file,新建一个打开的空白画布
在对话框输入:
Plain Text
使用pencil mcp
在当前活跃的画布上
然后重新设计「QQ音乐」手机App的所有主要界面,界面要像苹果公司的设计风格,浅色系
它就会给到一个基本的效果。不满意?没关系,继续跟它说。让它改,直到你觉得"差不多了"为止。
假设你现在觉得这个设计差不多了,那可以跟它说这个设计差不多了,你开始写代码吧
这时候它会让你选择一个技术栈,一般网页产品选Next.js就好,这里可以先选推荐
等它跑完,一个初步成型的产品就出来了,之后可以再根据前面学的来替换组件、优化功能等等
当然也可以直接在已有项目里调用Pencil,比如我在虚拟男友项目里,给了它这样一条指令
Plain Text
你用pencil MCP帮我修改一下人物对话页面,尽量让每个人物的对话页面都能和人物特点匹配上
人物对话界面就变得高级一点了,不再是所有角色一套界面
作业
1.
选择开源组件,打磨你的产品,让你的产品首页,像一个成熟、专业的产品!
2.
选择性尝试课程后面介绍的高级方法,总结适合你自己的工作流程。

十四、Bug和报错:你的产品一定会出问题,然后呢

Bug 是什么?报错是什么?

你在 TRAE 里改了一行代码,保存,切到浏览器刷新——
页面空白了。
或者终端里突然冒出一堆红色英文,滚了好几屏。
你不知道自己做错了什么。
这节课,就是为了这一刻准备的。
很多人把这两个词混着用,但它们其实不是一回事。
Bug 是代码逻辑出了问题,产品的行为不符合你的预期。比如你的哄哄模拟器应该输出"哄好了",结果输出了"undefined"。程序没有崩,还在跑,但结果是错的——这就是 Bug。
报错 是程序跑到某一步,直接停下来了,系统主动告诉你"这里出了问题,我没法继续"。
区别在于:Bug 是"跑完了但结果不对",报错是"没跑完,直接挂了"。
有一个反直觉的事实要先说清楚:报错其实是好事。 它说明程序发现了问题,并且把问题告诉了你。最难处理的情况反而是"没有报错,但结果不对"——那种情况你甚至不知道从哪里开始找。
所以看到红色的报错信息,第一反应不是慌,而是:好,它告诉我哪里出了问题了。

报错出现在哪里?

这是这节课最重要的知识点。
你用的是 Next.js,它是一个前后端一体的框架。这意味着你的代码有两个运行的地方——一部分跑在浏览器里(前端),一部分跑在服务器上(后端)。这两部分出了问题,报错会出现在完全不同的地方
如果你不知道去哪里找报错,就算报错就在眼前,你也看不见它。
客户端报错——在浏览器里看
什么情况下会出现客户端报错?​
页面上的交互逻辑出了问题。比如某个按钮点了没有反应,页面渲染崩了显示空白,或者某个数据没有正确显示出来。这些都是前端的问题,报错会出现在浏览器里。
在哪里看?​
浏览器的开发者工具,具体是里面的 Console(控制台)​ 标签。
打开方式:
Mac:Cmd + Option + J
Windows:F12,然后点顶部的 Console 标签
或者在浏览器网页中点击右键 - 检查
打开之后,你会看到一个黑色或白色的面板。如果有报错,会显示红色的文字。
怎么复制报错信息?​
建议复制全部红色的错误
在Console区域点击右键,选择 “Copy Console”
服务端报错——在 TRAE 终端里看
什么情况下会出现服务端报错?​
API 接口出了问题。比如连不上数据库,AI 接口调用失败,某个服务端函数逻辑出错,或者你的 .env 配置有问题。这些都是后端的问题,报错不会出现在浏览器里,而是出现在 TRAE 的终端里。
在哪里看?​
TRAE 界面底部的 Terminal(终端)​ 面板。就是那个你用来跑 npm run dev 的地方。项目在运行的时候,服务端的所有日志和报错都会输出在这里。
看到红色的❌ 没,遇到这种情况,说明是报错信息。
怎么复制报错信息?​
在终端里直接用鼠标选中文字,Cmd+C / Ctrl+C 复制。
注意:终端里的报错有时候会很长,不需要全部复制。找到最核心的那几行——通常是写着 Error: 或者 TypeError: 开头的那一行,加上它下面紧跟着的几行。
其实报错信息是人话,你可以尝试直接阅读。也可以复制报错信息,发给AI,让它帮忙理解
会得到这样的提示

一句话记住区分方法

客户端报错
服务端报错
出问题的地方
页面交互、前端渲染
接口、数据库、AI 调用
在哪里看
浏览器 Console
TRAE 终端
遇到报错,先问自己:我点的是页面上的东西,还是在调用某个接口?​ 这一个判断,就能帮你找对地方。
也可以养成好习惯:遇到报错,同时从浏览器、服务端Terminal去查,有啥就复制啥,所有报错都复制。

先别慌,先读懂报错在说什么

很多人看到报错的第一反应是"看不懂英文",然后直接关掉不看,或者把整屏内容截图扔给 AI。
其实报错信息有固定的结构,不需要全看懂,只需要找到几个关键的部分。
错误类型 通常出现在最前面,比如:
TypeError — 类型错误,比如你把一个 undefined 当成对象用了
SyntaxError — 语法错误,代码写法有问题
ReferenceError — 引用错误,用了一个不存在的变量
Cannot read properties of undefined — 试图读取一个不存在的东西的属性
错误描述 紧跟在后面,用英文写的,但很多时候字面意思就能看懂。比如 Cannot find module './utils' 就是"找不到这个文件",is not a function 就是"这个东西不是一个函数"。
错误位置 是最有用的信息,通常长这样:at HomePage (app/page.tsx:42:15),意思是错误发生在 app/page.tsx 这个文件的第 42 行第 15 个字符。
你不需要全部理解,只需要把错误类型 + 错误描述这两部分找出来,再加上文件名和行数,就够用了。
你能尝试理解一下,刚才报错截图的类型、错误描述、文件名、行数吗?
我标注出来了
红色:报错类型
绿色:报错描述
黄色:报错文件 和 行数
蓝色:具体报错的代码

遇到报错,走这个流程

不要凭感觉乱改。报错有固定的处理流程,每次都走一遍,比随机试错高效得多。
第一步:判断报错在哪里。
客户端还是服务端?打开浏览器 Console 和 TRAE 终端,看哪边有红色信息。
第二步:复制核心报错信息。
如果你知道报错在哪儿:不用全复制。找到 Error: 那一行,加上下面几行,复制出来。
如果不太清楚:可以全复制。不过不如只复制报错那样高效。
第三步:扔给 TRAE AI。
打开 TRAE 右侧的 AI 对话框,把报错粘贴进去,加一句描述。不需要自己分析,让 AI 先告诉你这是什么意思。
第四步:让 AI 定位到出问题的代码。
TRAE 的 AI 可以直接跳转到对应的文件和行数。告诉 AI 报错位置,让它帮你找到那段代码,然后解释问题出在哪里。
第五步:修完之后验证。
按 AI 的建议修改,保存,切回浏览器刷新,看问题是否消失。如果还有报错,重复这个流程。
这个流程的核心逻辑是:不要猜,要问。 你不需要自己搞懂每一条报错,你需要的是一个可以重复使用的处理方式。

这样问 AI,修得更快

同样是把报错扔给 AI,问法不同,得到的回答质量差很多。
直接把报错截图发过去,AI 能帮你,但可能要来回几轮才能定位到问题。如果你多提供一点上下文,AI 第一次回答就能给出准确的修改建议。
推荐这个固定格式,遇到报错直接套用:
Plain Text
我在用 Next.js 本地开发,遇到了一个报错。
报错信息如下: [粘贴报错]
我刚才做的操作是: [描述你做了什么,比如"修改了 app/api/chat/route.ts 里的一个函数"]
请帮我分析是什么原因,怎么修。
这个模板的逻辑是:给 AI 三个信息——技术环境(Next.js 本地开发)、报错内容(复制粘贴)、触发操作(你刚才改了什么)。有了这三个信息,AI 不需要猜测背景,可以直接给出针对性的回答。
"我刚才做的操作是"这一行很多人会漏掉,但它往往是最关键的。很多报错不是因为你写错了什么,而是因为你改了 A,影响到了 B。告诉 AI 你改了什么,它才能顺着这条线找到根本原因。

有些报错不是你写错了

有一类报错很容易让人陷入自我怀疑:翻来覆去检查自己的代码,什么问题都找不到,但项目就是跑不起来。
这种情况很可能不是你的代码有问题,而是环境出了问题
几种常见的情况:
依赖没装,或者装了一半失败了。 报错信息里会出现 Cannot find module 或者 Module not found。解决方法:在 TRAE 终端里重新跑一次 npm install,等它装完再启动。
环境变量没配,或者配错了。 你的 .env 文件里少了某个 Key,或者 Key 的名字写错了。报错信息里通常会出现 undefined 或者某个变量读不到。解决方法:对照 .env.example 文件(如果有的话)检查 .env 里的内容。
端口被占用。 上一次启动的进程没有正常关闭,新的进程想用同一个端口,就起不来了。报错信息里会出现 EADDRINUSE 或者 address already in use。解决方法:在 TRAE 终端里按 Ctrl+C 关掉当前进程,等几秒再重新 npm run dev
Node.js 版本不对。 项目要求的 Node 版本和你本地安装的不一样。这种情况比较少见,但如果其他方法都试过了还是不行,可以问 AI 检查一下版本。
这类报错有一个共同特点:报错信息不会指向你写的代码,而是指向 node_modules 文件夹、启动脚本,或者某个配置文件。遇到这种情况,先告诉 AI "这是环境问题还是代码问题",让它帮你判断方向。

作业

故意搞坏,再修好。
这次的作业不是做新东西,而是练一次完整的报错处理流程。
1.
打开你的哄哄模拟器项目,确保它现在是能正常跑起来的状态。
2.
打开任意一个文件,故意制造一个报错——比如删掉一个变量名里的一个字母,或者删掉一个函数末尾的括号。
3.
保存,看报错出现在哪里——是浏览器 Console,还是 TRAE 终端?
4.
把报错复制出来,用这节课的提问模板扔给 TRAE AI。
5.
按 AI 的指引找到问题,修好,让项目重新跑起来。
这个练习的目的不是考验你,而是让你在"安全环境"里走一遍完整的流程。真正出问题的时候,你已经练过了,知道该怎么做,不会慌。

三个要点

报错是好事,说明程序发现了问题并告诉了你
Next.js 有两种报错:客户端看浏览器 Console,服务端看 TRAE 终端
遇到报错不要猜,复制核心信息,用固定模板问 AI
下一节课:服务器、部署、上线——你的产品怎么从电脑跑到全世界?

十五、服务器、部署、上线:你的产品怎么从电脑跑到全世界?【重要】

本地跑得好,为什么别人打不开?

你做好了一个网站,在自己电脑上打开 localhost:3000,一切正常。你把这个地址发给朋友,朋友说:打不开。
这不是 bug,这是正常的。localhost 的意思是"本机",这个地址只有你自己的电脑才能访问。你的项目现在只活在你的电脑里,朋友的浏览器根本不知道去哪里找它。
要让全世界的人都能访问你的网站,你需要做一件事:把代码放到一台永远开着的电脑上,然后给它一个公开的网址。 这台永远开着的电脑,就是服务器。而把你的代码搬上去、让它跑起来的过程,就叫做部署,部署完成、网站可以公开访问了,就叫做上线
听起来很复杂?以前确实复杂。你需要自己租服务器、配置操作系统、安装运行环境、处理安全证书……光是这些就能把新手劝退。但现在有了 Vercel,这一切都不需要你操心了。
这节课的目标很简单:课程结束后,你能把自己的 Next.js 项目部署到 Vercel,拿到一个真实的网址,发给任何人都能打开。

认识 Vercel:你的免费"网站发布机"

Vercel 是什么
Vercel 是一个专门为前端开发者设计的云平台。如果一定要用一句话描述它,我会说:Vercel 是一台自动化的网站发布机——你把代码推上去,它帮你把网站编译好、部署好、分发到全球,全程不需要你动手。
更具体地说,Vercel 同时做了三件事:
第一,自动构建。你的 Next.js 项目需要先经过编译(pnpm build),把源代码变成浏览器能直接运行的文件。Vercel 帮你自动完成这一步。
第二,全球 CDN 加速。Vercel 在全球几十个城市都有服务器节点。你的网站部署后,它的内容会被分发到这些节点上。用户访问时,会自动从离他最近的节点加载,速度极快。一个在上海的用户和一个在纽约的用户,访问的是同一个网站,但加载速度都很快。
第三,零配置服务器。传统部署需要你自己配置服务器环境,而 Vercel 把这些全部封装好了。你不需要懂 Linux,不需要懂 Nginx,不需要懂 SSL 证书,直接用就行。
为什么 Vercel 和 Next.js 天然搭配
这里有一个很多人不知道的事实:Next.js 就是 Vercel 公司开发的。
Vercel 这家公司创造了 Next.js 这个框架,然后又做了 Vercel 这个部署平台。所以 Next.js 的每一个新特性——服务端组件、边缘函数、图片自动优化——在 Vercel 上都是零配置开箱即用。框架和平台是同一个团队设计的,自然配合得天衣无缝。
如果你把 Next.js 项目部署到其他平台,可能需要额外的配置才能让某些功能正常工作。但在 Vercel 上,你什么都不用做,直接就是最优状态。
你用的那些 AI 产品,也是这么上线的
如果你觉得这套工具"听起来是给小项目用的",看看这些名字:OpenAI 的官网、Claude(Anthropic)的官网,都是用 Next.js 构建、部署在 Vercel 上的。你现在每天用的 ChatGPT 官网,背后的技术栈和你正在学的是同一套。nextjs.org
这不是说你的项目和 OpenAI 一样大,而是说:这套工具是经过顶级团队验证的,它足够好,足够可靠,值得认真学。
一句话了解什么是Vercel: Vercel是网站的自动发布机器!
免费计划够用吗
Vercel 的 Hobby(免费)计划对个人项目完全够用:无限个人项目、自动 HTTPS、全球 CDN、每月一定量的函数调用额度。你不需要一开始就付费。
关于自定义域名(比如把 yourname.vercel.app 换成 yourproduct.com),我们后面有单独一课来讲,这节课先把网站跑起来。

部署前的准备

在正式部署之前,有几件事需要先确认好,这能帮你避免很多不必要的麻烦。
第一步:确认代码已经在 GitHub 上。 Vercel 是通过读取你的 GitHub 仓库来部署的,所以你的代码必须先 push 到 GitHub。如果你还没做这一步,回去复习 GitHub那一课,把项目推上去再来。
第二步:本地先跑一遍 pnpm build 在终端里进入你的项目目录,运行这个命令:
Bash
pnpm build
这个命令会模拟 Vercel 的构建过程。如果本地构建成功,Vercel 上大概率也会成功;如果本地就报错了,先在本地修好再 push,比推上去之后在 Vercel 的 Build Logs 里找原因要快得多。
第三步:检查你的 .env 文件。 打开项目根目录,看看有没有 .env.env.local 文件。如果有,里面存的是你的 API Key 等敏感信息。这个文件绝对不能 push 到 GitHub(.gitignore 里应该已经有了),但你需要记住里面有哪些变量,因为等会儿要在 Vercel 上重新填写一遍。

第一次部署:手把手操作

注册并登录 Vercel
打开 vercel.com,点击右上角的 "Sign Up"。强烈建议选择"Continue with GitHub",用 GitHub 账号直接登录。这样 Vercel 和 GitHub 就自动打通了,后面导入仓库会方便很多。
新建项目
登录后你会看到 Vercel 的 Dashboard(控制台)。点击右上角的 "Add New...""Project"
导入 GitHub 仓库
Vercel 会列出你 GitHub 账号下的所有仓库。找到你要部署的项目,点击右边的 "Import" 按钮。
如果你的仓库没有出现在列表里,点击 "Adjust GitHub App Permissions",重新授权 Vercel 读取你的仓库。
配置项目
导入后,Vercel 会进入项目配置页面。这里有几个地方需要注意:
Framework Preset(框架检测):Vercel 会自动识别你的项目是 Next.js,这里应该已经自动选好了,不需要改。
Root Directory(根目录):大多数情况下保持默认就行,除非你的项目结构比较特殊。
Environment Variables(环境变量):这里先展开,把你 .env (或 .env.local) 文件里的所有变量填进去。
填好之后,点击 "Deploy"
等待构建完成
点击 Deploy 之后,Vercel 会开始构建你的项目。你能看到实时的构建日志在滚动。整个过程通常需要 1~3 分钟。
如果一切顺利,你会看到一个撒花的成功页面,上面有你的网站链接。点开它(点击预览图片即可),你的网站已经在线了。
点击预览图片,可以看到,Vercel自动给你分配了一个 xxx.vercel.app 的域名
这个域名,是可以全世界范围内访问的!!!你可以发给你的朋友!
💡
Tips:这个域名可能需要配置海外网络环境才能访问。 后面会讲到,你可以绑定自己的域名。
恭喜你,你的项目刚刚上线了。

自动部署:push 一次,全世界同步更新

第一次部署完成后,以后的更新会变得极其简单——你只需要像平时一样把代码 push 到 GitHub,Vercel 会自动帮你重新部署,网站自动更新,不需要你做任何额外操作。
这背后是怎么做到的
这个机制叫做 Webhook(网络钩子)。当你在 Vercel 里导入 GitHub 仓库的时候,Vercel 悄悄在你的 GitHub 仓库里注册了一个 Webhook。
你可以把 Webhook 理解成一个"门铃":每当你的 GitHub 仓库有新的 push,GitHub 就会自动"按一下这个门铃",向 Vercel 发送一个通知,告诉它"这个仓库有新代码了"。Vercel 收到通知后,立刻自动拉取最新代码,重新构建,部署上线。
整个流程是这样的:
Plain Text
你在 TRAE 里修改代码
       ↓
commit + push 到 GitHub
       ↓
GitHub 触发 Webhook,通知 Vercel
       ↓
Vercel 自动拉取代码、重新构建
       ↓
新版本上线,全世界可访问
你不需要登录 Vercel,不需要点任何按钮,一切自动发生。
如何验证自动部署在工作
改一行代码,比如把首页的某个标题文字改一下,commit 并 push 到 GitHub。
然后打开 Vercel Dashboard,你会看到一条新的部署记录正在进行中(状态显示为 "Building")。等它变成绿色的 "Ready",刷新你的网站,改动已经生效了。
改坏了怎么办
有时候你 push 了一个有问题的版本,网站挂了。不要慌,Vercel 保留了所有历史部署记录。在 Dashboard - Deployments 里找到上一个状态为 "Ready" 的部署,点击它,然后点右上角的 "...""Redeploy",网站就会回滚到那个版本。
当然,线上回滚后,你也记得去Github回滚和修改你的代码哦

环境变量:线上最容易踩的坑

为什么本地好好的,上线就不行了
这是新手部署后最常遇到的问题:本地功能完全正常,部署到 Vercel 之后,API 调用失败、页面报错、功能不见了。
原因几乎都指向同一个地方:环境变量没有配置。
你在本地开发时,API Key 存在 .env.local 文件里,Next.js 会自动读取它。但这个文件只在你的电脑上,它没有被 push 到 GitHub(也不应该被 push,否则你的 Key 就暴露了),所以 Vercel 的服务器根本不知道这个文件的存在。
结果就是:Vercel 构建出来的网站,拿不到任何 API Key,所有需要调用 API 的功能都会失败。
在 Vercel 里配置环境变量
我们第一次部署的时候,前面的内容,已经告诉你如何设置环境变量。
但是如果你要修改、或者后续增加其他环境变量的话,
进入你的项目 Dashboard,点击顶部导航的 "Settings",然后在左侧菜单找到 "Environment Variables"
在这里,你需要把本地 .env.local 文件里的每一个变量都手动添加进来。添加方式很简单:填写变量名(Key)和变量值(Value),然后点 "Save"。
注意环境的选择:Vercel 允许你为 Production(生产环境)、Preview(预览环境)、Development(开发环境)分别配置不同的值。一般情况下,勾选 Production 就够了。
填完之后必须重新部署
这是很多人踩的第二个坑:环境变量填好了,但网站还是不正常。
原因是:修改环境变量不会自动触发重新部署。 你需要手动触发一次部署,新的环境变量才会生效。
回到项目 Dashboard,找到最新的那条部署记录,点击 "...""Redeploy",等它构建完成,环境变量就生效了。
一个好习惯
每次在本地新增了一个环境变量,马上去 Vercel 的 Environment Variables 里同步添加一遍。不要等到部署之后发现功能不正常再来找原因。

在 Vercel 里看线上日志

网站上线后,出了问题怎么找原因?答案是:看日志。 Vercel 提供了完整的日志系统,这是你线上排查问题的第一个工具。
两种日志,用途不同
Build Logs(构建日志):记录的是 Vercel 构建你的项目时发生的一切。如果部署失败(Build Failed),来这里找原因。常见的错误信息会直接显示在这里,比如"找不到某个依赖"、"TypeScript 类型错误"、"某个环境变量未定义"。
Runtime Logs(运行时日志):记录的是网站跑起来之后发生的事情。用户访问了某个 API、服务端逻辑报错、某个函数执行失败——这些都会出现在 Runtime Logs 里。
怎么找到日志
进入你的项目 Dashboard - Deployments,点击某一条部署记录,进入部署详情页,你能看到 "Build Logs" 标签页。
Runtime Logs 在另一个地方:回到项目 Dashboard,点击顶部导航的 "Logs" 标签(有些版本叫 "Functions")。这里显示的是你的 API 路由和服务端函数的实时运行日志。
一个真实的排查场景
假设你的网站部署成功了,但 AI 对话功能没有反应。你应该怎么做:
第一步,打开 Runtime Logs,找到最近的报错记录。你可能会看到类似这样的信息:
Plain Text
Error: OPENAI_API_KEY is not defined
这就是答案——环境变量没配,或者变量名写错了。回到 Settings → Environment Variables,检查一遍,修正后 Redeploy,问题解决。
这个排查思路和 上一课的内容的完全一样,只是从本地的终端换到了 Vercel 的 Logs 页面。换了地方,但方法没变:看报错信息,找根因,修复,验证。

常见故障排查

部署这件事,踩坑是正常的。以下是新手最常遇到的三类问题:
Build Failed(构建失败)。最常见的原因有两个:一是本地没有先跑 pnpm build 验证,代码里有 TypeScript 错误或者 ESLint 报错,到 Vercel 上才暴露出来;二是环境变量缺失,构建过程中需要读取某个变量但读不到。解决方法:先在本地跑 pnpm build,确保通过;然后检查 Vercel 的 Environment Variables 是否完整。
404 页面。网站能打开,但某些页面显示 404。通常是路由配置问题,检查你的页面文件路径是否正确,或者图片、静态资源的路径是否用了本地绝对路径(比如 /Users/yourname/...),这在线上是不存在的。
功能不正常但没有明显报错。网站能打开,但 AI 对话没反应、数据加载不出来、某个按钮点了没效果。这种情况 90% 是环境变量的问题——要么漏填了,要么填错了变量名,要么填完之后忘记 Redeploy。打开 Runtime Logs,找到对应的报错信息,基本上就能定位。
作业
这节课的作业只有一个目标:把你的 Next.js 项目真正部署上线。
完成后,提交以下三样东西:
第一,线上访问链接——你的 .vercel.app 网址,确保点开能正常访问。
第二,环境变量配置截图——Vercel Dashboard 里 Environment Variables 页面的截图,Key 的名称要显示,Value 的值用星号遮盖就行。
第三,一次报错修复记录——在部署过程中,故意制造一个问题(比如先不填环境变量),截图 Build Logs 或 Runtime Logs 里的报错信息,然后修复它,再截图成功后的状态。这个练习的目的是让你熟悉"出错 → 看日志 → 找原因 → 修复"这个流程,以后遇到真实问题你就不会慌了。
小结
这节课讲了一件事:让你的代码从电脑里出来,变成全世界都能访问的网站。
你现在已经掌握了:Vercel 是什么、为什么用它、怎么一步步完成第一次部署、自动部署的原理、环境变量为什么是上线最容易踩的坑、以及出了问题去哪里找日志。
有了 GitHub + Vercel 这套组合,上线这件事从"玄学操作"变成了一个可以重复执行的标准流程。下一课,我们来讲域名——把那个 .vercel.app 的网址换成你自己的域名。

十六、域名:给你的产品起个好记的名字

让你的网站有个好记的地址:域名

上一课,你把自己的项目部署到了 Vercel,拿到了一个类似 your-project.vercel.app 的网址。你可以把这个链接发给朋友,他们能打开,网站真的上线了。
但你有没有觉得这个网址有点奇怪?它很长,不好记,一眼看上去也不像一个"正经产品"的地址。
这就是这节课要解决的问题:给你的产品一个属于自己的、好记的网址。 这个网址,就叫做域名。
💡
注意: 如果你当前的产品只是用来练手,可以不要实际购买域名!
省点钱,给你的正式产品买域名。

域名是什么

域名就像是你网站在互联网上的"门牌号码"。想象一下,如果互联网是一座巨大的城市,那么每个网站就是这座城市里的一栋建筑,而域名就是这栋建筑的地址。当人们想要访问你的网站时,他们只需要在浏览器中输入这个域名,就能直接找到你。
你每天都在用域名,只是没有注意到:
google.com(谷歌)
bilibili.com(哔哩哔哩)
github.com(GitHub)
没有域名,人们就需要记住复杂的 IP 地址(比如 104.21.33.168)才能访问你的网站。IP 地址是互联网世界里真正的"门牌号",但它对人类来说太难记了。域名的本质,就是给这串数字起了一个好记的名字。
当你在浏览器里输入 google.com,背后发生的事情是:浏览器去问一个叫做 DNS(域名解析服务器)的系统,"请问 google.com 对应的 IP 地址是多少?"DNS 回答之后,浏览器才知道该去哪里找 Google 的服务器。这个查询过程在几毫秒内完成,你完全感觉不到。

域名的组成部分

shop.example.com 为例,一个完整的域名由几个部分组成:
顶级域名(TLD)是最右边的部分,比如 .com.org.io.ai.cn。它是由国际机构统一管理的,你不能自己创造一个新的顶级域名。
主域名是中间那部分,比如 example。这是你花钱买的部分,也是最能体现你品牌个性的部分。主域名加上顶级域名,合在一起就是 example.com,这就是你真正购买和拥有的东西。
子域名是最左边的部分,比如 shop。子域名是免费的,只要你拥有主域名,就可以自己创建任意多个子域名,不需要额外付费。
这里有一个中文世界常见的说法误区:很多人把主域名叫做"顶级域名",其实是不准确的。顶级域名是 .com.cn 这些,主域名是 example 这部分。不过大家已经习惯这么说了,日常交流不影响,你心里清楚就好。

子域名:你已经用过它了

说到子域名,你其实已经用过了,只是当时没注意到。
上一课在 Vercel 部署完,拿到的那个地址——your-project.vercel.app——就是一个子域名。vercel.app 是 Vercel 公司的主域名,他们把它的子域名免费分配给每一个部署的项目使用。你的项目名叫什么,子域名就是什么。
大公司也是同样的玩法,只不过规模更大,一个主域名下面挂着几十个子域名,每个对应不同的产品:
mail.google.com——Google 邮件
maps.google.com——Google 地图
docs.google.com——Google 文档
news.163.com——网易新闻
sports.163.com——网易体育
music.163.com——网易云音乐
同一个主域名,不同的子域名指向完全不同的产品和服务。将来你的产品做大了,也可以用 app.yoursite.com 放主产品、blog.yoursite.com 放博客、docs.yoursite.com 放文档。这节先了解概念,实际操作等你需要的时候再研究。
还有一个你每天都见到的子域名:wwwwww.example.com 里的 www 其实就是一个子域名,它是互联网早期的习惯,代表"World Wide Web"。现在大多数网站已经不强制要求 www 了,直接输入 example.com 就能访问,www 只是为了兼容老习惯保留着。

怎么给自己的产品起一个好域名

买域名之前,先想清楚买什么。一个好域名,通常符合这几个标准:
——越短越好,最好在 10 个字符以内。长域名不好记,打字也容易出错。
好拼——用英文字母,避免数字和连字符(-)。数字和连字符在口头传播时很容易造成混淆,比如"是数字 1 还是字母 l?"
和产品有关联——域名最好能让人一眼猜到这是什么产品。notion.solinear.appfigma.com 都是很好的例子,简短、独特、和品牌强绑定。
顶级域名怎么选——这里给你一个简单的决策框架:
.com 是最通用的选择,适合所有类型的产品,用户最熟悉,看到 .com 会本能地信任。如果你想要的 .com 域名还没被注册,优先拿下它。
.io 在开发者工具和 SaaS 产品里非常流行,比如 github.ionotion.so.so 同理)。如果你的产品面向开发者或技术用户,.io 是个不错的选择。我的Fast3D产品,使用的就是.io 域名 https://fast3d.io
.ai 是现在 AI 产品的热门后缀,很多知名 AI 工具都在用。如果你做的是 AI 产品,.ai 能让人一眼知道你的定位。不过 .ai 的价格比 .com 贵不少,大约每年 $70~80,量力而行。我的一个视频产品 wula.ai 使用的就是 .ai 域名 https://wula.ai
.app 适合 Web App 类产品,Google 管理这个后缀,强制要求 HTTPS,安全性有保证。我的Raphael AI产品,使用的就是 .app 域名 https://raphael.app
这门课的建议:我们做的是面向海外用户的产品,优先选 .com。如果 .com 被注册了,再考虑 .io.ai。不建议选 .cn.com.cn,那是面向国内用户的,和我们的方向不符。

怎么脑暴域名

直接把你的产品描述告诉 AI,让它帮你想 10 个候选域名。比如:"我做了一个帮助用户自动整理邮件的 AI 工具,帮我想 10 个适合的英文域名,要求简短好记"。AI 给出候选之后,再去域名注册平台搜索,看哪些还没被注册。
使用域名搜索工具查询可用域名
在购买域名之前,你需要先确认你心仪的域名是否已被他人注册。互联网上的域名是独一无二的,就像每个人的身份证号码一样,不可能有两个完全相同的。
推荐工具:Instant Domain Search
这个工具的优势在于:
即时反馈:当你输入时就能立即看到结果,不需要等待
替代建议:如果你想要的域名已被注册,它会推荐类似的可用域名
多种顶级域名:可以同时查看多种后缀(如。com、.net、.org 等)的可用性
价格估算:显示不同域名的预估价格
如何使用域名搜索工具:
2.
在搜索框中输入你想要的域名(不需要输入。com 等后缀)
3.
系统会立即显示该域名在各种后缀下的可用状态
4.
绿色表示可注册,红色表示已被注册
5.
浏览推荐的替代域名,可能会有更好的选择

在 Namecheap 购买域名

💡
再次提醒: 如果你当前的产品只是用来练手,可以不要实际购买域名!
省点钱,给你的正式产品买域名。
我们推荐使用 Namecheap 购买域名。理由很简单:价格透明、界面清晰、在全球开发者圈里口碑很好,而且支持支付宝付款,对国内用户友好。
打开 namecheap.com,在首页的搜索框里输入你想要的域名的前缀,点击搜索。
搜索结果会告诉你这个域名是否可以注册,以及价格。如果你想要的域名已经被别人注册了,页面会同时推荐一些相近的可用选项。
找到心仪的域名后,点击 "Add to Cart",然后结账。结账时有几个注意事项:
第一,注册年限。默认是 1 年,你也可以选择 2 年或更长。如果你确定要用这个域名,可以考虑多买几年,通常有一点折扣。
第二,WhoisGuard 隐私保护。Namecheap 提供免费的域名隐私保护,开启后别人查询你的域名注册信息时,看不到你的真实姓名和联系方式。这个建议开启。
第三,续费价格。首年购买价格有时候有促销,但续费价格可能不同。在结账前,点击域名旁边的价格详情,确认续费价格,避免第二年续费时被价格吓到。
完成支付后,你就拥有了这个域名。

将域名连接到 Vercel

买好域名之后,它还是一个"空地址"——有名字,但不知道指向哪里。我们需要告诉它:当有人访问这个域名时,把他们带到我的 Vercel 项目上去。这个过程叫做 DNS 配置,听起来复杂,实际上按步骤操作很简单。
第一步:在 Vercel 里添加域名
打开 Vercel,进入你的项目,然后在左侧菜单选择 "Domains"
在输入框里填入你刚买的域名(比如 yourproduct.com),点击 "Add"
Vercel 会给你显示一段 DNS 配置信息,通常是一条 CNAME 记录,内容大概长这样:
如果是 www,通常走 CNAME 到 cname.vercel-dns.com
如果是裸域(yourdomain.com),按域名商和 Vercel 的提示走对应记录(A/AAAA/ALIAS/ANAME,因平台不同)。 现在的流程图是对的,但文字上最好显式点,避免学员卡在裸域上。
CNAME 是什么?​ CNAME 的全称是 Canonical Name,可以理解为"别名"。它的作用是把你的域名指向另一个域名,由那个域名再去解析出最终的 IP 地址。比如把 www.yourproduct.com 指向 cname.vercel-dns.com,之后 Vercel 自己负责管理背后的 IP,你不需要关心。这样做的好处是,Vercel 的服务器 IP 变了,你这边完全不用改任何配置。
A 记录是什么?​ A 记录是最基础的 DNS 记录类型,直接把域名映射到一个 IPv4 地址(如 76.76.21.21)。浏览器拿到这个 IP,就知道该去哪台服务器请求你的网站了。
为什么裸域不能用 CNAME?​ 这是一个技术限制:根据 DNS 标准,裸域(yourdomain.com,没有任何前缀)不允许设置 CNAME 记录,因为裸域还需要同时承载 MX(邮件)等其他记录,而 CNAME 会与它们冲突。所以裸域只能用 A 记录直接指向 IP。部分域名商提供了 ALIAS 或 ANAME 这类"扁平化 CNAME"的变体,行为上像 CNAME 但符合裸域的规范,Vercel 也会在合适时推荐你使用。
💡
提示:下面我这里是示例!你按照Vercel平台提示你的来。
先不要关掉这个页面,把这些信息记下来,待会儿要用到。
第二步:在 Namecheap 里配置 DNS
打开 Namecheap,登录后点击右上角的 "Account""Dashboard",找到你刚买的域名,点击 "Manage"
进入域名管理页面后,点击 "Advanced DNS" 标签。这里显示的是这个域名的 DNS 记录列表。
点击 "Add New Record",按照 Vercel 给你的信息填写。
填完后点击保存(绿色的对勾按钮)。
第三步:等待 DNS 生效
DNS 配置保存后,不会立刻生效。互联网上有很多 DNS 服务器,它们需要时间同步最新的配置。通常情况下,几分钟到半小时内就会生效;极少数情况下可能需要几个小时,最长不超过 48 小时。
你可以回到 Vercel 的 Domains 页面,刷新一下,看看域名状态是否变成了绿色的 "Valid"
第四步:验证
状态变绿之后,在浏览器里输入你的域名,回车。如果你的网站正常显示,恭喜你,域名绑定成功了。
还有一件事你不需要操心:HTTPS 证书。你可能注意到网址前面有一个小锁头,那代表网站是加密的、安全的。这个证书 Vercel 会自动帮你申请和配置,完全不需要你手动操作。

关于备案:为什么我们暂时不需要担心

你可能听说过"网站备案"这件事,或者有人告诉你"域名要备案才能用"。这里解释清楚,避免不必要的担心。
备案是中国大陆的一项规定:在中国大陆境内的服务器上运行的网站,必须向工信部备案,否则可能被屏蔽。 这个规定针对的是服务器的物理位置,而不是域名本身。
我们用的 Vercel,服务器节点在海外(美国、欧洲等地),不在中国大陆境内。所以我们的网站不需要备案,也不适用这条规定。
此外,这门课的定位本来就是做面向海外用户的产品,所以我们从一开始就绕开了备案这件事。
如果将来你的产品需要专门服务国内用户,那时候再考虑迁移到国内服务器并完成备案,现阶段完全不用管。

作业

💡
再次提醒: 如果你当前的产品只是用来练手,可以不要实际购买域名!
省点钱,给你的正式产品买域名。
这节课的作业就一件事:给你的产品买一个域名,然后绑定到你的 Vercel 项目上。
完成后提交两样东西:
第一,能正常访问的自定义域名链接——在浏览器里打开你的新域名,截图,地址栏里显示的是你的域名而不是 .vercel.app
第二,Vercel Domains 页面截图——显示你的域名状态为绿色 "Valid"。
域名是你产品对外的第一张名片,值得认真挑一个。

十七、人机验证:有人用机器人来薅你的产品,怎么办?

真实案例:免费额度一夜被薅光

先说一个真实发生的事。
我们第一期有个同学,做了一个 AI 产品。产品本身很不错,上线后他给每个新注册用户提供了一点免费额度——每人可以免费用 5 次 AI 功能。
想法很好对不对?让用户先体验一下,觉得好再付费。
结果上线没几天,他发现自己的 AI 接口额度莫名其妙被烧光了。
一看后台数据:短时间内涌入了大量注册用户,但这些用户注册完、用完免费额度就消失了。
怎么回事?
有人写了一个脚本,自动注册账号。一个脚本一秒能注册几十个号,每个号用完 5 次免费额度就扔掉,再注册一个新的。
你花钱买的 AI 接口额度,就这样被"薅"走了。
这不是"万一"会发生的事,这是"一定"会发生的事。 只要你的产品有任何免费资源——免费 AI 调用、免费生成图片、免费发消息——就一定会有人想办法用机器人来薅。
这节课,我们学习:保护你的产品不被机器人薅羊毛。
💡
学完之后,你需要掌握三件事:
1.
心智模型——理解"人机验证"是什么,为什么你的产品一定需要它。
2.
技术实操——能用 Cloudflare Turnstile 给你的产品加上人机验证,前端 + 后端都搞定。
3.
选型判断——知道 Turnstile 的三种模式分别适合什么场景,能根据自己产品的需要做选择。

人机验证是什么?

其实你每天都在遇到人机验证,只是可能没注意过。
登录某些网站的时候,页面上会出现一个小方块,上面写着"请证明你是真人"或者"I'm not a robot"。有时候你需要点一下勾,有时候什么都不用做它就自己通过了。
有时候会让你从一堆图片里选出红绿灯。
这个东西就叫人机验证,英文叫 CAPTCHA。
它的本质很简单:在用户和你的产品之间,加一道"安检"。这道安检对真人用户来说几乎没有感觉,但对机器人来说是一道过不去的坎。
打个比方:你开了一家餐厅,门口排了很长的队。突然你发现队伍里混进来一堆机器人,它们不是来吃饭的,是来免费拿餐巾纸的。你怎么办?
在门口放一个保安,让每个进来的人证明自己是真人。
人机验证就是你产品门口的保安。

为什么一定要加人机验证?

你可能觉得:我的产品还很小,谁会来薅我?
但现实是:在互联网上,"薅羊毛"是自动化的。不需要有人专门盯着你的产品,有些人写了通用脚本,专门扫描互联网上的新产品,只要发现有免费额度就自动开始注册。
你的产品"小",不代表安全。恰恰相反,小产品往往更容易被薅——因为你没做任何防护。
更关键的是:你的 AI 接口是花钱的。
不管你用的是 Coze 的 API、OpenAI 的 API 还是其他模型的 API,每次调用都在烧钱。一个真实用户一天可能用个三五次,但一个脚本一分钟就能调用几百次。
没有人机验证的产品,就像一家门口没有锁的店。不是说一定会有人来偷东西,但你把门敞开着,迟早会出事。
所以这不是可选的,是必须的。只要你的产品有注册功能,就应该加人机验证。

为什么选 Cloudflare Turnstile?

市面上做人机验证的服务不少。最有名的是 Google 的 reCAPTCHA,就是那个让你点红绿灯、选自行车的。
但我们推荐用 Cloudflare Turnstile。理由很简单:
第一,免费。
完全免费,没有请求量限制。
对于我们这种个人开发者来说,这是最重要的。reCAPTCHA 虽然也有免费版,但有调用量限制,超了就要付费。
第二,用户体验好。
大多数情况下,用户什么都不用做。Turnstile 会在后台自动判断你是不是真人——通过分析你的浏览器行为、鼠标轨迹等信号。只有在它"拿不准"的时候,才会让用户点一下勾。
而 reCAPTCHA 那个点红绿灯的体验,说实话,挺烦人的。
第三,不收集隐私数据。
Google 的 reCAPTCHA 会收集大量用户数据——毕竟 Google 的商业模式就是数据。
Cloudflare Turnstile 明确承诺不会用人机验证收集的数据做广告或用户追踪。
第四,接入简单。
前端加一个组件,后端加一个验证接口,就搞定了。

Turnstile 的三种模式

在 Cloudflare 后台创建 Turnstile 组件的时候,它会让你选一种模式。一共有三种:
模式一:Managed(托管模式)⭐ 推荐
这是默认模式,也是我最推荐的模式
"Managed"的意思是:交给 Cloudflare 来管。它会根据访问者的风险等级,自动决定怎么验证:
如果 Cloudflare 觉得你明显是真人(比如你的浏览器行为很正常),直接放行,用户什么都看不到。
如果 Cloudflare 觉得有点可疑,但还不确定,会在页面上显示一个小方块,让用户点一下勾。
如果 Cloudflare 觉得你很可能是机器人,验证会失败。
注意:即使在最严格的情况下,Turnstile 也不会让你去点红绿灯或者选自行车。 最多就是点一下勾,体验比 reCAPTCHA 好很多。
适合场景: 绝大多数产品,用这个就够了。
模式二:Non-Interactive(非交互模式)
页面上会显示一个小组件(带一个加载动画),但用户完全不需要做任何操作。验证在后台自动完成,小组件只是用来告诉用户"我们正在验证中"。
和 Managed 的区别是:Managed 在某些情况下可能要求用户点一下勾,而 Non-Interactive 永远不会。
适合场景: 你希望页面上有一个可见的"验证中"的提示,但又不想让用户做任何操作。比如一个支付确认页面,你希望用户知道系统正在做安全检查。
模式三:Invisible(隐身模式)
完全看不见。用户不会看到任何验证组件,也不会有任何加载动画。整个验证过程完全在后台静默完成。
适合场景: 你希望页面上完全没有验证的痕迹,追求极致的视觉简洁。
但要注意一点:如果你用 Invisible 模式,Cloudflare 要求你在隐私政策里说明你使用了 Turnstile。因为用户看不到任何提示,所以你有义务在其他地方告知。
我的建议
直接用 Managed,不用纠结。 它是最智能的——低风险用户自动放行,高风险用户才需要互动。对于我们大多数产品来说,这是最好的平衡。

实操:给纸片人男友加上人机验证

好,理论说完了,开始动手。
我们要做的事情很明确:在纸片人男友的注册页面上,加上 Cloudflare Turnstile 人机验证。 这样,机器人就没法自动注册账号来薅你的 AI 额度了。
整个过程分五步。
第一步:注册 Cloudflare,创建 Turnstile 组件
1.
打开 Cloudflare 官网,注册一个账号(如果你已经有了就直接登录)。
2.
登录后,在左侧菜单找到 Turnstile,点进去。
3.
点击 Add Widget(添加组件)。
4.
填写信息:
Widget name(组件名称):随便写,比如"纸片人男友"。
Domain(域名):填你产品的域名。
预发布/生产:用真实域名(比如你的 vercel.app 域名或正式域名)
本地测试:可以填localhost进行验证。如果没有正式创建 Widget,也可以先使用下面这个 Cloudflare 官方提供的测试 Key 跑通测试流程:
Plain Text
官方测试 Key:
  - sitekey: 1x00000000000000000000AA
  - secret: 1x0000000000000000000000000000000AA
Widget Mode(组件模式):选 Managed
5.
点击 Create(创建)。
创建完成后,你会看到两个 Key:
Site Key(站点密钥):这个放在前端代码里,是公开的,用户可以看到。
Secret Key(私密密钥):这个放在后端代码里,绝对不能公开。
把这两个 Key 先复制下来,马上要用。
第二步:把 Key 配到环境变量里
还记得前面课程讲的环境变量吗?密码不能直接写在代码里。这两个 Key 也是一样的道理——尤其是 Secret Key,必须放在环境变量里。
打开你项目根目录下的 .env.local 文件(如果没有就创建一个),加上这两行:
Plain Text
NEXT_PUBLIC_TURNSTILE_SITE_KEY=你的SiteKey
TURNSTILE_SECRET_KEY=你的SecretKey
注意看:Site Key 前面有 NEXT_PUBLIC_ 前缀。还记得吗?在 Next.js 里,带这个前缀的环境变量才能在前端代码中使用。Site Key 本身就是要在前端展示的,所以需要这个前缀。
而 Secret Key 没有这个前缀——它只在后端(服务端)使用,绝对不能暴露给前端。
第三步:安装前端组件
在终端里运行:
Bash
npm install @marsidev/react-turnstile
这是一个封装好的 React 组件库,让你可以很方便地在 Next.js 里使用 Turnstile。
第四步:在注册页面加上人机验证组件(前端)
打开你纸片人男友的注册页面文件(大概在 app/register/page.tsxapp/login/page.tsx 这种路径下),找到注册表单的位置。
在表单里,"注册"按钮的上面,加上 Turnstile 组件:
TypeScript
import { Turnstile } from '@marsidev/react-turnstile'

// 在表单里面,注册按钮的上方
<Turnstile
  siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
  onSuccess={(token) => {
    setTurnstileToken(token)
  }}
/>
这段代码做了什么?
1.
在页面上渲染一个 Turnstile 人机验证组件。
2.
当用户通过验证后,Turnstile 会返回一个 token(令牌)。
3.
我们把这个 token 存到一个状态变量里(setTurnstileToken),等用户点"注册"的时候,一起发给后端。
这个 token 就像一个"通行证"——Turnstile 说"这个人我检查过了,是真人",然后给你一张通行证,你拿着这张通行证去后端报到。
localhost样式
线上样式
第五步:后端验证——最关键的一步!
很多人做到第四步就以为搞定了,但其实这才走了一半。
为什么?因为前端的验证是可以被绕过的。
想象一下:一个稍微懂技术的人,可以直接跳过你的注册页面,用代码直接往你的后端接口发请求。他的请求根本没有经过 Turnstile 验证。如果你的后端不检查 token,那人机验证就形同虚设。
就像你在餐厅门口放了个保安,但厨房后门大开着。
保安有什么用?
所以你必须在后端也做一次验证。
打开你的注册 API 文件(大概在 app/api/auth/register/route.ts 这种路径下),在处理注册逻辑之前,加上 Turnstile 验证:
TypeScript
// 从请求体中拿到前端传来的 Turnstile token
const { turnstileToken, ...registrationData } = await request.json()

// 去 Cloudflare 验证这个 token 是不是真的
const verifyResponse = await fetch(
  'https://challenges.cloudflare.com/turnstile/v0/siteverify',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      secret: process.env.TURNSTILE_SECRET_KEY,
      response: turnstileToken,
    }),
  }
)

const verifyResult = await verifyResponse.json()

// 如果验证失败,直接拒绝
if (!verifyResult.success) {
  return Response.json(
    { error: '人机验证失败,请重试' },
    { status: 403 }
  )
}

// 验证通过,继续正常的注册流程...
这段代码做了什么?
1.
从前端传来的请求里取出 Turnstile token。
2.
把这个 token 连同你的 Secret Key 一起,发给 Cloudflare 的验证接口。
3.
Cloudflare 会告诉你:这个 token 是真的还是假的。
4.
如果是假的(success: false),直接返回错误,不允许注册。
5.
如果是真的,继续正常的注册流程。
几个重要细节:
Cloudflare 的验证接口地址是:https://challenges.cloudflare.com/turnstile/v0/siteverify
每个 token 只能验证一次。用过的 token 再去验证,会返回 timeout-or-duplicate 错误。
token 的有效期是 5 分钟(300 秒)。超时未验证的 token 也会失效。
Secret Key 只在后端使用,绝对不能出现在前端代码里

看看效果

做完上面五步,重新启动你的项目,打开注册页面。
正常用户看到的: 注册表单下面多了一个小方块,大概几秒钟就自动通过了(显示一个绿色的勾),然后正常点击注册按钮就行。很多时候用户甚至注意不到它的存在。
机器人遇到的: 脚本直接调用你的注册接口 → 没有 Turnstile token → 后端验证失败 → 注册被拒绝。 或者脚本试图伪造 token → 发给 Cloudflare 验证 → 返回失败 → 注册被拒绝。
你的 AI 额度安全了。

延伸阅读:人机验证的前世今生

你可能好奇:为什么 Cloudflare Turnstile 不用点红绿灯,而 Google reCAPTCHA 要?
这其实是两代技术的区别。
第一代:文字验证码。
就是那种扭曲变形的字母数字,让你照着输入。
这是很早期的方案,但现在 AI 识别文字的能力太强了,基本已经废了。
第二代:图片验证。
Google reCAPTCHA v2,就是让你从九宫格里选红绿灯、人行横道、自行车。
这个方案有效,但体验很差——有时候你选了半天它还说你选错了。
而且你知道吗? Google 其实在用你的选择来训练它的 AI 模型(帮 Google 标注自动驾驶的训练数据)。
第三代:无感验证。
Cloudflare Turnstile 和 Google reCAPTCHA v3 都属于这一代。
它们不再依赖"让人类做一道题",而是通过分析浏览器行为(鼠标怎么动的、页面怎么加载的、有没有用自动化工具)来判断你是不是真人。
大多数时候用户完全无感。
Turnstile 属于第三代方案,而且免费、不收集隐私数据——所以我们选它。

要点回顾

1.
只要你的产品有注册功能或免费资源,就一定要加人机验证。 不是"万一"的事,是"一定"会被薅的。
2.
选 Cloudflare Turnstile,用 Managed 模式。 免费、体验好、接入简单。
3.
前端和后端都要验证。 前端加组件获取 token,后端调 Cloudflare 接口确认 token。只做前端验证等于没做。
4.
Token 一次性使用,5 分钟有效。 这是 Cloudflare 的安全机制。
5.
Secret Key 绝对不能暴露在前端代码里。 放在环境变量里,只在后端使用。

课后作业

给你的纸片人男友的注册页面加上 Cloudflare Turnstile 人机验证。
具体要求:
1.
在 Cloudflare 后台创建一个 Turnstile 组件,拿到 Site Key 和 Secret Key。
2.
在注册页面的前端加上 Turnstile 验证组件。
3.
在注册接口的后端加上 token 验证逻辑。
4.
测试:正常注册能成功,不带 token 的请求会被拒绝。
提示: 如果你不知道代码该怎么写,可以直接把这节课的实操部分复制给 AI,让它帮你改代码。记住,你不需要自己写代码,但你需要知道"要做什么"和"为什么这么做"。

十八、云存储:AI 生成的图片链接过期了,怎么办?

1. 真实场景:纸片人男友的照片消失了

你的纸片人男友越来越受欢迎了。你给它加了一个新功能:用户可以让男友"画一幅画"送给自己——调用 AI 图片生成接口,生成一张专属的情话配图。
用户小美昨晚让纸片人男友画了一幅"星空下的告白",当时看到图片觉得好感动,截图发了朋友圈。
今天她想再看一眼,点开产品——图片没了。一个灰色的框,上面写着"图片无法加载"。
怎么回事?
因为 AI 生成图片的接口(比如 DALL·E、Midjourney API、或者 Coze 里集成的图片生成),返回给你的通常是一个临时链接。这个链接有有效期——有的 1 小时,有的 24 小时,有的几天。过了有效期,链接就失效了,图片就没了。
你没有"保存"这张图片,你只是拿到了一个临时的"查看链接"。链接过期,图片就蒸发了。
这就像别人给你发了一条阅后即焚的消息,你当时看了,没保存,之后就再也看不到了。
这一课就解决这个问题:让你的产品里 AI 生成的文件(图片、音频、视频)永久保存下来,随时可以访问。
💡
学完之后,你需要掌握三件事:
1.
心智模型——理解"云存储"是什么,为什么 AI 产品特别需要它。
2.
技术实操——能用 Cloudflare R2 给你的产品搭建一个文件存储空间,把 AI 生成的图片存进去。
3.
选型判断——知道为什么选 R2 而不是 AWS S3 或其他方案。

2. 云存储是什么?

解决办法很简单:在 AI 生成图片之后,立刻把图片下载下来,存到一个属于你自己的地方。
存到哪里?存到云存储里。
你可以把云存储想象成一个网盘——类似百度网盘、iCloud、Google Drive。你把文件传上去,它给你一个永久的链接,任何人通过这个链接都能访问到这个文件。
但跟个人网盘不同的是,云存储是给程序用的。它的特点是:
1.
通过代码上传和下载。 你的程序调用一个接口,就能把文件传上去,不需要手动操作。
2.
文件有永久的公开链接。 上传完成后,你拿到一个 URL,用户通过浏览器就能直接访问。
3.
可以存任何类型的文件。 图片、视频、音频、PDF,什么都行。
4.
稳定、快速、不会丢。 专业的云存储服务商帮你管着这些文件,不用担心丢失。
简单说:你的数据库存文字(用户名、聊天记录),云存储存文件(图片、视频、音频)。两者配合,你的产品才完整。

3. "S3 协议"——云存储的普通话

在云存储领域,有一个绕不开的名词:S3
S3 是 Amazon(亚马逊)最早做的云存储服务,全名叫 Simple Storage Service(简单存储服务)。它是这个领域的开山鼻祖,也是用得最多的。
后来其他云厂商也做了自己的云存储服务,但为了让大家方便切换,它们几乎都兼容 S3 的接口协议。也就是说,你写一套代码对接 S3,不用改代码就能换到其他云存储服务——因为大家"说的是同一种语言"。
这就像 USB 接口:不管你买的是什么牌子的手机,充电线都是 USB-C。S3 协议就是云存储界的 USB-C。
所以当你听到"S3 兼容",意思就是:用同一套代码就能对接。
市面上主要的 S3 兼容云存储服务:

4. 为什么选 Cloudflare R2?

我们推荐用 Cloudflare R2。原因有三个:
流量免费(这是最关键的)
云存储的费用分两部分:存储费(文件占了多少空间)和流量费(文件被下载/访问了多少次)。
对于大多数产品来说,流量费才是大头。想想看:你存一张图片可能只有 1MB,但这张图片被 1000 个用户看了,流量就是 1000MB = 1GB。
AWS S3 的流量费是 $0.09/GB。 看起来不多?
如果你的产品有一万个用户,每人每天看 10 张图片,一个月的流量费就可能上千美元。
Cloudflare R2 的流量费是 $0。零。免费。 不管你的文件被下载多少次,不收流量费。
对独立开发者来说,这个区别是决定性的。你的产品刚起步,用户量不确定。如果用 S3,你可能某天突然被一笔意外的流量账单吓到。用 R2,你完全不用担心这个。
有免费额度
R2 每个月提供:
10 GB 免费存储空间
1000 万次免费读取操作
100 万次免费写入操作
对于一个起步阶段的产品,这个免费额度足够用很久。
超出免费额度后的价格也很便宜:存储 $0.015/GB/月。存 100GB 的图片,一个月才 $1.5。
S3 兼容,代码通用
R2 完全兼容 S3 协议。这意味着:
你用的是 AWS 官方的 SDK(代码库),不需要学新东西。
如果将来你想换到 S3 或者其他服务,改一下配置就行,代码不用动。
网上关于 S3 的教程和资料,绝大多数都适用于 R2。
总结:免费流量 + 免费额度 + S3 兼容 = 独立开发者的最佳选择。

5. 实操:给纸片人男友加上云存储

我们要做的事情是:当 AI 生成一张图片后,立刻把这张图片存到 R2 里,拿到一个永久链接,保存到数据库中。以后用户再来看,用的是永久链接,图片永远不会消失。
整个流程是这样的:
分六步来做。
注意:Cloudflare 的更新频率很高,有可能一周就更新一次界面,下面的操作步骤,很可能你学的时候,名称变了、位置也挪了……所以,取其精髓,在页面里多点点多摸索。
你大概知道要找什么,就一定能找到。
第一步:创建 R2 存储桶(Bucket)
"存储桶"是 R2 里的一个概念,你可以理解为一个"文件夹"。一个存储桶就是一个独立的存储空间。
2.
左侧菜单找到 R2 对象存储,点进去。
3.
点击 创建存储桶。新用户可能需要先订阅,但 Cloudflare 的免费额度很高,订阅后不会直接扣费。
4.
填写名称,比如 paper-boyfriend-images
5.
选择位置:如果你的用户主要在国内,选亚太地区;如果面向全球,选自动。
6.
点击创建。
第二步:开启公开访问
默认情况下,R2 存储桶里的文件是私密的,只有你自己能访问。但我们的图片需要让用户在浏览器里直接看到,所以要开启公开访问。
1.
进入刚创建的存储桶。
2.
找到 设置 选项卡。
3.
找到 公开访问(Public Development URL)
4.
有两种方式开启:
R2.dev 子域名:最简单,Cloudflare 给你一个免费的子域名,比如 pub-xxx.r2.dev。适合开发和测试。
自定义域名:绑定你自己的域名,比如 files.你的域名.com。适合正式上线。
先用 R2.dev 子域名就够了,后面随时可以换成自定义域名。
第三步:创建 API Token
你的代码需要通过 API 来上传文件,所以需要一个"通行证"——API Token。
1.
在 R2 页面右上角,找到 管理 R2 API 令牌
2.
点击 创建 API 令牌
3.
权限选择 对象读和写
4.
指定存储桶:选你刚才创建的那个。
5.
点击创建。
创建完后会给你三样东西:
Access Key ID
Secret Access Key
这三个都要存好,尤其是 Secret Access Key 只显示一次!
第四步:配置环境变量
把刚才拿到的信息加到项目的 .env.local 文件里:
Plain Text
R2_ACCESS_KEY_ID=你的AccessKeyID
R2_SECRET_ACCESS_KEY=你的SecretAccessKey
R2_ENDPOINT=https://你的账户ID.r2.cloudflarestorage.com
R2_BUCKET_NAME=paper-boyfriend-images
R2_PUBLIC_URL=https://pub-xxx.r2.dev
最后一行 R2_PUBLIC_URL 是你的存储桶的公开访问地址,上传完文件后拼接文件名就能访问。
第五步:安装 S3 SDK 并写上传代码
在终端里安装 AWS S3 SDK:
Bash
npm install @aws-sdk/client-s3
没错,虽然我们用的是 Cloudflare R2,但装的是 AWS 的 SDK——因为 R2 兼容 S3 协议。
然后在项目里创建一个工具文件,比如 lib/r2.ts
TypeScript
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'

// 初始化 S3 客户端(指向 R2)
const s3Client = new S3Client({
  region: 'auto',
  endpoint: process.env.R2_ENDPOINT,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  },
})

// 上传文件到 R2
export async function uploadToR2(
  fileBuffer: Buffer,
  fileName: string,
  contentType: string
): Promise<string> {
  await s3Client.send(
    new PutObjectCommand({
      Bucket: process.env.R2_BUCKET_NAME,
      Key: fileName,
      Body: fileBuffer,
      ContentType: contentType,
    })
  )

  // 返回永久的公开链接
  return `${process.env.R2_PUBLIC_URL}/${fileName}`
}
这段代码做了什么?
1.
创建了一个 S3 客户端,但 endpoint 指向 R2(不是 AWS)。
2.
uploadToR2 函数:接收文件内容、文件名、文件类型,把文件上传到 R2,然后返回一个永久的公开链接。
第六步:在 AI 生成图片后,保存到 R2
现在来把这个上传功能接入你的业务逻辑。找到你的纸片人男友生成图片的 API 接口(比如 app/api/generate-image/route.ts),修改逻辑:
TypeScript
import { uploadToR2 } from '@/lib/r2'
import { nanoid } from 'nanoid'

// 1. 调用 AI 接口生成图片,拿到临时链接
const aiResponse = await callAIImageAPI(prompt)
const tempImageUrl = aiResponse.imageUrl  // 这是临时链接

// 2. 下载这张图片
const imageResponse = await fetch(tempImageUrl)
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer())

// 3. 生成一个唯一的文件名
const fileName = `images/${nanoid()}.png`

// 4. 上传到 R2,拿到永久链接
const permanentUrl = await uploadToR2(imageBuffer, fileName, 'image/png')

// 5. 把永久链接存到数据库
await db.insert(generatedImages).values({
  userId: currentUser.id,
  imageUrl: permanentUrl,  // 存的是永久链接!
  prompt: prompt,
  createdAt: new Date(),
})

// 6. 返回永久链接给前端
return Response.json({ imageUrl: permanentUrl })
整个流程串起来了:
1.
AI 生成图片,给了一个临时链接。
2.
你的后端立刻把这张图片下载下来。
3.
然后上传到你自己的 R2 存储桶。
4.
拿到一个永久的链接,存进数据库。
5.
返回给前端的是永久链接。
从此以后,用户随时打开产品,都能看到之前生成的图片。不会消失了。在 R2 上也能看到历史生成的图片。
请看下面两个图的对比,左边的图是我们用CDN以前的,可以看到图片的地址是个临时地址,这个地址很快就会被删除掉那用户就无法通过这个地址看到图片了。
右边的图是我们改成R2云存储之后的,这个URL在我们自己的控制范围之内,由我们来决定是否删除,什么时候删。

6. 看看效果

做完上面六步,试一下:
1.
让纸片人男友生成一张图片。
2.
图片正常显示——这个链接是你的 R2 永久链接。
3.
过了一天,再打开——图片还在。
4.
打开 Cloudflare 后台,进入 R2 存储桶——能看到你上传的图片文件。
搞定。

7. 延伸阅读:还能存什么?

云存储不只是存 AI 生成的图片。在一个完整的产品里,你可能需要存:
用户上传的头像。 用户注册后上传自己的头像,需要存到云存储。
AI 生成的音频。 比如纸片人男友的"语音消息"——AI 生成的 TTS 音频文件。
AI 生成的视频。 短视频类 AI 产品会大量用到。
用户上传的文件。 比如用户上传一份简历让 AI 帮忙优化。
导出的 PDF 报告。 AI 分析完数据后生成的报告。
记住一条简单的分工:只要是"文件",就存云存储。只要是"数据"(文字、数字),就存数据库。

要点回顾

1.
AI 生成的图片/视频/音频通常是临时链接,会过期。 你必须自己保存一份。
2.
云存储 = 给程序用的网盘。 通过代码上传文件,拿到永久链接。
3.
S3 是云存储的通用协议。 R2、OSS、COS 都兼容 S3,学一套代码到处用。
4.
选 Cloudflare R2。 流量免费、有免费额度、S3 兼容——独立开发者的最佳选择。
5.
流程:AI 生成 → 下载到你的后端 → 上传到 R2 → 拿到永久链接 → 存数据库。 这五步记住就行。

课后作业

给你的纸片人男友加上云存储功能。
具体要求:
1.
在 Cloudflare 后台创建一个 R2 存储桶,开启公开访问。
2.
创建 API Token,配置好环境变量。
3.
在项目中写一个上传文件到 R2 的工具函数。
4.
修改 AI 生成图片的接口,把临时链接替换成永久链接。
5.
测试:生成一张图片,过 24 小时后再打开,图片还在。
提示: 跟上节课一样,如果你不知道代码该怎么写,把这节课的实操部分复制给 AI,让它帮你改。但你要理解这个流程——先下载,再上传,拿到永久链接存起来。这个思路比代码本身更重要。

十九、定时任务:你睡了,产品还能自己干活

上一课留的问题

上节课我们给纸片人男友加了发邮件的能力。你写好了 sendDailyLoveLetterToAll() 函数——它能遍历所有用户,给每个人发一封 AI 生成的情话邮件。
但有一个问题没解决:谁来调用这个函数?
你总不能每天早上 8 点准时爬起来,打开电脑,手动运行一下代码吧?如果你某天睡过头了,用户今天就收不到情话了?
你需要一个"闹钟",每天早上 8 点自动帮你按一下"发送"按钮。
这就是定时任务
这节课就解决这一件事:让你的产品能在指定的时间自动执行任务,不需要你手动触发。
💡
学完之后,你需要掌握三件事:
1.
心智模型——理解"定时任务"是什么,它在产品运营中有多重要。
2.
技术实操——能用 cron-job.org + Next.js API Route 实现定时触发。
3.
安全意识——知道怎么保护你的定时任务接口,防止被外人随便调用。

定时任务是什么?

定时任务,英文叫 Cron Job(读作"克朗 乔布")。
"Cron" 这个词来自 Unix/Linux 系统,是一个几十年前就有的工具,专门用来"在指定时间自动执行某个命令"。
你可以把定时任务想象成一个智能闹钟。普通闹钟到点了响铃叫你起床。定时任务到点了去调用你的一个接口——"嘿,该发情话邮件了""嘿,该检查一下过期订单了""嘿,该给三天没来的用户发召回邮件了"。
闹钟叫醒的是你,定时任务叫醒的是你的产品。
定时任务在产品里的应用场景特别多:
每天早上 8 点给用户发一封情话邮件(我们这节课要做的)
每小时检查一下有没有过期的订单需要关闭
每天凌晨统计一下昨天的用户数据,生成报表
每周一早上给用户发一封"本周精选"推荐
每 5 分钟检查一下 AI 任务队列,有没有没处理完的任务

原理:外部闹钟 + 你的接口

在传统的服务器上,定时任务直接跑在服务器里面——服务器自己设个闹钟,到点了自己执行。
但我们用的是 Vercel(或者类似的 Serverless 平台)。Serverless 的意思是"你没有一台一直开着的服务器"。你的代码只有在被请求的时候才会运行,没人请求的时候它就"睡着了"。
一个睡着的东西,怎么给自己设闹钟?
它设不了。
所以我们需要一个外部的闹钟服务——它一直醒着,到了指定时间,它来"敲你的门"(发一个 HTTP 请求到你的接口),把你叫醒,让你干活。
流程是这样的:
你的产品负责"做事",外部闹钟负责"叫你"。两者配合,产品就能自己运转了。

为什么用 cron-job.org?

外部定时任务服务有好几个选择。我们推荐 cron-job.org
第一,完全免费。
不限制定时任务的数量(合理使用),支持最短每分钟执行一次,不需要绑信用卡。
第二,足够简单。
注册后就是一个后台界面,填上"要访问的 URL"和"什么时候访问",就搞定了。不需要写代码,不需要配置文件。
第三,稳定可靠。
这个服务已经运行了很多年,社区口碑不错。
它还提供执行失败的邮件通知——如果你的接口挂了,它会发邮件告诉你。
第四,不依赖部署平台。
不管你用 Vercel、Netlify 还是其他平台部署,cron-job.org 都能用。
它只是定时访问一个 URL,不在乎这个 URL 跑在哪里。将来你换了部署平台,定时任务不用动,改一下 URL 就行。
当然还有其他方案,简单了解一下:

Cron 表达式:怎么告诉闹钟"什么时候响"

在配置定时任务的时候,你需要用一种叫 Cron 表达式 的格式来指定时间。
别被这个名字吓到。Cron 表达式就是 5 个数字/符号,分别代表:
Plain Text
分钟  小时  日期  月份  星期几
 ┃     ┃     ┃     ┃     ┃
 0     8     *     *     *
这个例子的意思是:每天早上 8 点 0 分执行。
* 的意思是"每一个"——每一天、每一月、每一个星期几。所以 0 8 * * * 就是"在分钟=0、小时=8 的时候,不管哪天哪月星期几,都执行"。
再来几个例子:
好消息是 cron-job.org 支持设置时区。
你可以直接选"Asia/Shanghai",这样表达式里写 0 8 * * * 就是北京时间早上 8 点,不用自己算时差。
记不住也没关系。
搜索"crontab guru"(crontab.guru),这是一个在线工具,能帮你可视化地编辑和理解 Cron 表达式。输入一个表达式,它马上告诉你"下一次执行是什么时候"。

实操:让纸片人男友每天早上自动发情话

分四步完成。
第一步:写一个定时任务 API 接口
在你的 Next.js 项目里,创建一个新的 API Route 文件:app/api/cron/daily-email/route.ts
TypeScript
import { sendDailyLoveLetterToAll } from '@/lib/email'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  // 第一步:验证请求是否合法
  const authHeader = request.headers.get('authorization')
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json(
      { error: '未授权访问' },
      { status: 401 }
    )
  }

  // 第二步:执行任务——给所有用户发情话邮件
  try {
    await sendDailyLoveLetterToAll()
    return NextResponse.json({
      success: true,
      message: '每日情话发送完成',
      time: new Date().toISOString(),
    })
  } catch (error) {
    console.error('每日情话发送失败:', error)
    return NextResponse.json(
      { error: '发送失败' },
      { status: 500 }
    )
  }
}
这就是一个普通的 Next.js API Route,只不过它不是给用户点击触发的,而是等着被外部闹钟调用。
注意开头的授权验证——这是安全防线,非常重要,后面专门讲。
第二步:配置 CRON_SECRET 环境变量
.env.local 里加上:
Plain Text
CRON_SECRET=一个随机的长字符串
这个密钥的作用是:只有知道这个密钥的人才能触发你的定时任务接口。 你会把这个密钥配到 cron-job.org 里,这样只有 cron-job.org 发来的请求才能通过验证。
怎么生成一个随机字符串?最简单的办法,让 AI 给你生成一个就行。或者在终端里运行:
Bash
openssl rand -hex 32
会得到一个 64 位的随机字符串,足够安全了。
关键:记得在 Vercel(或你的部署平台)的环境变量设置里也加上这个 CRON_SECRET。本地的 .env.local 只在开发时有效,线上需要单独配置。
第三步:在 cron-job.org 创建定时任务
1.
打开 cron-job.org,注册一个账号。
2.
登录后,点击 Create cronjob(创建定时任务)。
3.
填写基本配置:
Title(标题):纸片人男友-每日情话
Schedule(执行计划):选择每天早上 8 点。cron-job.org 有可视化的时间选择器,你不需要手写 Cron 表达式,直接用界面选就行。
Timezone(时区):选 Asia/Shanghai
4.
设置请求头——这一步非常关键
找到 Headers 设置区域。
添加一个自定义 Header:
NameAuthorization
ValueBearer 你的CRON_SECRET值
这样 cron-job.org 每次请求你的接口时,都会带上这个密钥,你的后端验证通过才会执行。
5.
开启 失败通知(Notifications)——如果某次执行失败了,cron-job.org 会发邮件告诉你。强烈建议开启。
6.
点击 Create(创建)。
第四步:测试
创建完成后,cron-job.org 通常会让你手动触发一次来测试。点击 TEST RUN ,然后检查:
1.
你的邮箱——收到情话邮件了吗?
2.
cron-job.org 的执行日志——状态是不是 200(成功)?
3.
如果返回 401,说明 CRON_SECRET 没配对。检查 cron-job.org 里的 Header 和你线上环境变量里的值是不是一模一样。
4.
如果返回 500,说明你的发邮件逻辑有 bug。去部署平台看看日志。

安全:为什么一定要加验证?

你可能觉得多加了几行验证代码很麻烦。能不能省掉?
绝对不能。
想想看:你的定时任务接口是一个公开的 URL,比如 https://你的域名.com/api/cron/daily-email。如果没有任何验证,任何人只要知道这个 URL,就能触发它。
有人发现了你的接口地址,写个脚本每秒调用一次——你的所有用户就会被邮件轰炸。你的 Resend 免费额度(每月 3000 封)几分钟就烧光。用户投诉你发垃圾邮件,你的发件域名可能直接被 Gmail、Outlook 拉黑。
没有验证的定时任务接口 = 一个任何人都能按的核弹按钮。
加上 CRON_SECRET 验证之后,只有知道这个密钥的请求才能触发。别人访问这个 URL,只会得到一个 401 Unauthorized 错误。
这跟 C23 学的人机验证是同一个思路:不信任任何外部请求,先验证身份再干活。

延伸:更多定时任务的场景

"每天发情话"只是最简单的场景。随着你的产品变复杂,你会需要更多定时任务。
模式永远是一样的——写一个 API Route,在 cron-job.org 上配一个对应的定时任务:
用户召回:
每天晚上扫描数据库,找出"3 天没登录"的用户,给他们发一封"你去哪了,我想你了"的召回邮件。接口路径可以叫 /api/cron/recall-inactive-users
数据清理:
每周清理一次过期的临时文件、过期的验证码、过期的 Session。接口路径叫 /api/cron/cleanup
定时生成内容:
比如每天用 AI 生成一篇"今日情话合集",存到数据库,用户打开产品就能看到新鲜内容。接口路径叫 /api/cron/generate-daily-content
监控报警:
每 10 分钟检查一下你的 AI 接口是不是正常的,如果调不通了,给你自己发一封报警邮件。接口路径叫 /api/cron/health-check
每一个场景只需要:写一个 API Route → 在 cron-job.org 配一个定时任务 → 加上 CRON_SECRET 验证。
模式是固定的,场景是无限的。

回顾

1.
定时任务 = 智能闹钟。 到点了自动调用你的接口,让产品自己干活。
2.
我们用"外部闹钟 + 你的接口"来实现。 cron-job.org 负责"叫",你的 API Route 负责"做"。
3.
选 cron-job.org: 免费、简单、不依赖部署平台,支持设置时区和失败通知。
4.
Cron 表达式是时间的表达方式。 0 8 * * * = 每天 8 点。记不住就用 crontab.guru 。
5.
一定要加 CRON_SECRET 验证。 这是必须的,没有验证的定时任务接口是巨大的安全隐患。
6.
线上环境变量要单独配。 .env.local 只在本地有效,部署平台的环境变量才影响线上。

课后作业

让你的纸片人男友每天早上自动给用户发情话。
具体要求:
1.
在项目中创建 /api/cron/daily-email 接口,包含 CRON_SECRET 验证逻辑。
2.
在 cron-job.org 注册账号,创建一个定时任务,设为每天早上 8 点(北京时间)触发。
3.
配置好 Authorization Header,确保密钥匹配。
4.
手动触发一次,确认自己的邮箱收到了情话邮件。
5.
等一天,看看第二天早上 8 点是不是自动收到了。
提示: 开发阶段可以把定时任务设成"每 5 分钟执行一次"来快速测试,确认没问题后再改成每天一次。别忘了测完把频率改回来——不然你的用户会被邮件淹没的。

二十、邮件服务:想让产品主动联系用户,怎么办?

本课目标

这一课只解决一件事:让你的产品能够自动给用户发邮件。
💡
学完之后,你需要掌握三件事:
1.
心智模型——理解为什么你的产品需要"会发邮件",以及邮件在产品运营中扮演什么角色。
2.
技术实操——能用 Resend 给你的产品接入邮件发送功能,从注册欢迎信到每日情话。
3.
选型判断——知道为什么选 Resend 而不是 SendGrid 或 Mailgun。

真实场景:纸片人男友只会等你来找他

你的纸片人男友现在功能挺全的了——能聊天、能生成图片、还有注册登录。
但你有没有发现一个问题?
你的纸片人男友只会"等"。
用户来了就聊,用户走了就沉默。它从来不会主动联系用户。
时间一长,用户可能就把他忘了。
如果是 App 产品,它可以直接给你弹窗推送消息。
比如有人给你发微信,即使你没有打开微信,手机上照样会弹出一个通知。
这是 App 的能力。
但网页产品不行。
网页产品没办法像 App 那样弹一个系统通知到你手机上。
所以,网页产品想主动联系用户,最高频的方式就是——发邮件。
想想看你用过的那些网页产品:
你注册了一个网站,马上收到一封"欢迎加入"的邮件。
你三天没打开某个网页,收到一封"好久不见,想你了"的邮件。
你忘了密码,点"忘记密码",收到一封带验证码的邮件。
这些邮件都不是人发的,是产品自己发的。
你的纸片人男友如果能在用户注册的时候发一封"你好呀,我是你的专属男友"的欢迎信,每天早上发一封"早安情话",三天没来的时候发一封"你去哪了,我想你"——用户的留存率会完全不一样。
这就是我们这节课要做的事:教你的产品"说话"——通过邮件主动联系用户。

产品发邮件是怎么回事?

你平时发邮件用的是什么?Gmail、Outlook、QQ邮箱?
你打开邮箱客户端,写内容,填收件人,点发送。
这是在发邮件。
但产品发邮件不是这样的。你总不能每个用户注册的时候,自己手动打开 Gmail 给对方发一封欢迎信吧?
你白天发,晚上也发,半夜三点有人注册你爬起来发?
不可能的。
产品发邮件是通过邮件服务 API 来实现的。流程是这样的:
"邮件服务"就是一个专门帮你发邮件的中间商。
你告诉它:发给谁、标题是什么、内容是什么,它帮你搞定剩下的一切——找到对方的邮箱服务器、投递邮件、处理退信……
你就把它想象成一个邮局。你把信写好、贴上地址交给邮局,邮局负责送到对方手里。你不需要知道邮路怎么走、中间经过几个中转站。

产品发的邮件有哪些种类?

用专业术语来说,产品发的邮件分两大类:

事务性邮件(Transactional Email)

这是用户的操作触发的。比如:
注册成功后的欢迎邮件
忘记密码时的验证码邮件
下单后的订单确认邮件
支付成功后的收据邮件
特点:一对一发送,每个用户收到的内容不同,用户期待收到它。

营销性邮件(Marketing Email)

这类邮件是你主动发给用户的,用来促活、召回、推广。比如:
每日情话(定时给所有用户发)
"三天没见你了"的召回邮件
新功能上线通知
促销活动通知
特点:一对多发送,同一模板发给很多人,需要控制频率(太频繁用户会烦)。
对于纸片人男友这个产品,两种都需要。 注册欢迎信是事务性邮件,每日情话是营销性邮件。

为什么选 Resend?

市面上做邮件发送服务的有不少。最知名的几个:
我们选 Resend。
第一,开发者体验极好。
Resend 是近几年做邮件服务的新秀,API 设计非常简洁。发一封邮件只需要几行代码,不需要配置一堆乱七八糟的东西。
它的文档写得也好——清楚、现代、不啰嗦。
第二,免费额度够用。
免费版每月可以发 3000 封邮件
对于一个起步阶段的产品,足够了。
算一笔账:如果你有 100 个活跃用户,每人每月发 30 封(每天一封情话),刚好 3000 封。起步阶段一分钱不用花。
第三,跟 Next.js 天然配合。
Resend 的创始团队同时做了一个开源项目叫 React Email——用 React 组件来写邮件模板。
你已经在用 React 写页面了,现在写邮件模板也是同样的方式,不需要学新东西。
第四,发送成功率高。
邮件最怕的就是进垃圾箱。你辛辛苦苦写的欢迎信,用户压根没看到,躺在垃圾箱里吃灰。Resend 在邮件投递方面做了很多优化,发送成功率比很多老牌服务都好。

域名验证:让邮件从"你的域名"发出去

在开始写代码之前,有一件重要的事要先做:域名验证
为什么?因为如果你不验证域名,Resend 只能用它自己的域名帮你发邮件,邮件的发件人地址会是 xxx@resend.dev
这会带来两个问题:
1.
看起来不专业。 用户收到邮件一看,发件人不是你的产品名,而是一个陌生的 resend.dev,信任感直接归零。
2.
容易进垃圾箱。 邮箱服务商(Gmail、Outlook等)会检查发件人域名是否经过验证。没有验证的域名发出来的邮件,很大概率进垃圾箱。
验证域名的意思是:向 Resend 证明你是这个域名的主人。 这样你就能用 hello@你的域名.com 来发邮件了。

怎么验证?

1.
在 Resend 后台,点击 Domains,然后 Add Domain
2.
输入你的域名(比如 你的域名.com)。
3.
Resend 会给你几条 DNS 记录,让你添加到你的域名管理后台。
4.
打开你的域名管理后台(如果你的域名是在 Cloudflare 买的,就去 Cloudflare;Namecheap 的就去 Namecheap),把 Resend 给你的 DNS 记录添加上去。
5.
回到 Resend 后台,点击 Verify。验证通常几分钟就完成,有时候需要等几个小时(DNS 传播需要时间)。
如果你还在开发阶段,域名验证可以先跳过。 Resend 允许你用 onboarding@resend.dev 这个测试地址发邮件(只能发给你自己的邮箱),够用来测试了。正式上线前再验证域名。

实操:让纸片人男友给用户发欢迎邮件

好,开始动手。我们先实现最基本的场景:用户注册成功后,纸片人男友自动发一封欢迎邮件。
第一步:注册 Resend,拿到 API Key
1.
打开 Resend 官网,注册账号。
2.
登录后,在左侧菜单找到 API Keys
3.
点击 Create API Key
4.
权限选 Full Access(发送权限)。
5.
复制生成的 API Key。
注意:API Key 只显示一次!复制好存到安全的地方。
第二步:配置环境变量
在项目的 .env.local 文件里加上:
Plain Text
RESEND_API_KEY=re_你的APIKey
没有 NEXT_PUBLIC_ 前缀——发邮件是后端的事,API Key 绝对不能暴露在前端。
第三步:安装 Resend SDK
在终端里运行:
Bash
npm install resend
就一个包,够了。
第四步:写发邮件的工具函数
在项目里创建 lib/email.ts
TypeScript
import { Resend } from 'resend'

const resend = new Resend(process.env.RESEND_API_KEY)

export async function sendWelcomeEmail(
  userEmail: string,
  userName: string
) {
  await resend.emails.send({
    from: '纸片人男友 <hello@你的域名.com>',
    to: userEmail,
    subject: '你好呀,我是你的专属男友 💌',
    html: `
      <div style="font-family: sans-serif; max-width: 500px; margin: 0 auto;">
        <h2>Hi ${userName},欢迎来到纸片人男友!</h2>
        <p>从现在起,我就是你的专属男友了。</p>
        <p>有什么心事随时来找我聊,我会一直在这里等你。</p>
        <p>明天早上我会给你发一条早安消息,记得查收哦。</p>
        <br/>
        <p>—— 你的纸片人男友</p>
      </div>
    `,
  })
}
就这么几行代码。核心就是 resend.emails.send(),传入:
from:发件人(你的产品名 + 邮箱地址)
to:收件人(用户的邮箱)
subject:邮件标题
html:邮件内容(HTML 格式,可以加样式)
如果你还没验证域名,from 先用 onboarding@resend.dev,to 只能填你自己的邮箱。
第五步:在注册流程中调用
找到你的注册 API(比如 app/api/auth/register/route.ts),在用户注册成功之后,调用发邮件函数:
TypeScript
import { sendWelcomeEmail } from '@/lib/email'

// ... 注册逻辑(创建用户、存入数据库等)

// 注册成功后,发送欢迎邮件
await sendWelcomeEmail(user.email, user.name)

// 返回成功响应
return Response.json({ success: true })
一个小技巧: 如果你担心发邮件失败会影响注册流程(比如 Resend 接口临时不可用),可以把发邮件的代码用 try-catch 包起来。
TypeScript
// 注册成功后,发送欢迎邮件(失败不影响注册)
try {
  await sendWelcomeEmail(user.email, user.name)
} catch (error) {
  console.error('欢迎邮件发送失败:', error)
  // 不 throw,不影响注册流程
}
道理很简单:用户能注册成功才是最重要的事,欢迎邮件没收到不致命。 别因为一封邮件发不出去,把整个注册流程搞崩了。

进阶:让纸片人男友发每日情话

欢迎邮件搞定了。接下来做一个更有趣的:每天早上,纸片人男友给每个用户发一封情话邮件。
这里先写好发邮件的部分,"每天早上自动发"的部分留到下一课(定时任务)来解决。
先写一个"发每日情话"的函数:
TypeScript
export async function sendDailyLoveLetter(
  userEmail: string,
  userName: string
) {
  // 用 AI 生成今天的情话
  const loveLetter = await generateLoveLetter(userName)

  await resend.emails.send({
    from: '纸片人男友 <hello@你的域名.com>',
    to: userEmail,
    subject: `早安 ${userName},今天也想你了`,
    html: `
      <div style="font-family: sans-serif; max-width: 500px; margin: 0 auto;">
        <p>${loveLetter}</p>
        <br/>
        <p>—— 你的纸片人男友</p>
        <p style="color: #999; font-size: 12px;">
          想跟我聊天?<a href="https://你的域名.com">点这里回来找我</a>
        </p>
      </div>
    `,
  })
}
注意这里有个 generateLoveLetter 函数——它调用 AI 接口,根据用户名字生成一段个性化的情话。具体怎么调 AI 你已经会了(之前做哄哄模拟器的时候就在做这件事),这里就不展开了。
然后写一个"批量发给所有用户"的函数:
TypeScript
export async function sendDailyLoveLetterToAll() {
  // 从数据库拿到所有用户
  const users = await db.select().from(usersTable)

  for (const user of users) {
    try {
      await sendDailyLoveLetter(user.email, user.name)
    } catch (error) {
      console.error(`给 ${user.email} 发情话失败:`, error)
      // 某个用户失败不影响其他用户
    }
  }
}
这个函数准备好了,但谁来调用它?谁来"每天早上 8 点"触发它?
答案在下一课——定时任务(Cronjob)。这节课你先把邮件能力搭好,下节课教你让它自动运转。

延伸阅读:用 React Email 写更好看的邮件模板

你可能注意到了,上面的邮件内容是用 HTML 字符串写的。写几行还好,内容一多就很难维护——大量的 style="..." 内联样式,改一个颜色要改好几个地方。
Resend 团队做了一个开源项目叫 React Emailreact-email),让你用 React 组件来写邮件模板。就像你写页面一样写邮件:
Bash
npm install @react-email/components
TypeScript
import { Html, Head, Body, Container, Text, Button } from '@react-email/components'

export function WelcomeEmail({ userName }: { userName: string }) {
  return (
    <Html>
      <Head />
      <Body style={{ fontFamily: 'sans-serif' }}>
        <Container>
          <Text>Hi {userName},欢迎来到纸片人男友!</Text>
          <Text>从现在起,我就是你的专属男友了。</Text>
          <Button href="https://你的域名.com">来找我聊天</Button>
        </Container>
      </Body>
    </Html>
  )
}
然后在发邮件的时候,用 react 参数代替 html
TypeScript
import { WelcomeEmail } from '@/emails/welcome'

await resend.emails.send({
  from: '纸片人男友 <hello@你的域名.com>',
  to: userEmail,
  subject: '你好呀,我是你的专属男友 💌',
  react: WelcomeEmail({ userName }),
})
这样邮件模板就变成了可复用、好维护的 React 组件。当你的产品需要发很多种不同的邮件时,这个方案的优势就很明显了。
但起步阶段用 HTML 字符串完全够了,等邮件种类多了再考虑迁移到 React Email。

要点回顾

1.
产品需要"会说话"。 邮件是产品主动触达用户最基本的方式——欢迎信、验证码、召回、通知,都靠它。
2.
邮件通过 API 发送。 你的后端调用邮件服务的 API,邮件服务帮你送到用户邮箱。
3.
选 Resend。 免费 3000 封/月,API 简洁,跟 Next.js 配合好,发送成功率高。
4.
记得验证域名。 不验证的话邮件会从 resend.dev 发出去,容易进垃圾箱。正式上线前一定要验证。
5.
发邮件是后端的事。 API Key 放环境变量里,只在服务端代码中使用,不要暴露给前端。
6.
邮件发送失败不应该阻断主流程。 用 try-catch 包住,失败了只记日志,不影响用户正常使用。

课后作业

让你的纸片人男友学会发邮件。
具体要求:
1.
注册 Resend 账号,拿到 API Key,配置到环境变量。
2.
实现注册欢迎邮件:用户注册成功后,自动收到一封来自纸片人男友的欢迎信。
3.
写好 sendDailyLoveLetter 函数,准备好"每日情话"的发送逻辑(下节课我们让它自动运行)。
4.
(选做)验证你的域名,让邮件从你自己的域名发出去。
提示: 开发阶段先用 onboarding@resend.dev 当发件人,发给自己的邮箱测试。确认功能没问题后再验证域名。

二十一、用户反馈:你的用户想吐槽,往哪儿吐?

本课目标

这一课只解决一件事:让你的用户能方便地找到你,把意见告诉你。
💡
学完之后,你需要掌握三件事:
1.
心智模型——理解为什么"听到用户的声音"是产品活下来的关键。
2.
三种渠道——知道收集用户反馈的三条路:邮箱(必须有)、即时聊天(体验好)、社群(粘性强),每条路怎么搭。
3.
技术实操——能给你的产品接入 Crisp 在线聊天组件,让用户在产品里直接跟你说话。

真实场景:用户在骂你,但你不知道

你的纸片人男友现在挺像样的了。有注册登录,有人机验证,有云存储,还能每天自动发情话邮件。
用户小美用了三天,觉得"男友"说话太油腻了,每次都是"宝贝你是我的唯一"这种话,受不了。她想跟你说:能不能让男友说话正常一点?
但她找不到你。
产品里没有"联系我们"的入口。产品页面底部没有邮箱。她不知道你是谁,也不知道怎么找到你。
她会怎么办?
大概率:直接关掉,再也不来了。
她不会费劲去找你。这个世界上的产品太多了,一个不好用就换下一个。你丢掉了一个愿意给你提意见的用户——这种用户其实是最宝贵的,因为大部分不满意的人连骂都懒得骂你,直接走了。
愿意吐槽你的用户,是最有价值的用户。 但你得给他们一个吐槽的地方。
之前我们的优秀学员井然做了一次直播分享(https://fllv5.xetslk.com/sl/4yfrOF),他花了很大篇幅讲一件事——跟用户沟通,比写代码重要10倍。
他在项目早期遇到了不少主动提意见的用户,就是这些反馈帮助他不断改善产品。
他说,每一个主动联系你的用户,都是你未来的"种子金矿"——因为愿意开口的人,才是真正在乎你产品的人,也是你潜在的核心用户
而他用的方法,也是我们这节课要讲的。

用户反馈有多重要?

你可能觉得:我的产品功能都做好了,用户用就行了呗,为什么还要听他们说什么?
因为你觉得好用和用户觉得好用,是两码事。
你是开发者,你了解每个功能的设计意图。但用户不了解。
他们可能在一个你觉得很简单的地方卡住了。
他们可能有一个你从来没想到的使用方式。
他们可能发现了一个你从来没注意到的 Bug。
这就像你自己装修了一套房子,你觉得动线完美,住起来肯定舒服。但你妈来住了两天,跟你说:厨房门开的方向不对,每次端菜出来都得绕一圈。
你从来没注意到这个问题——因为你不做饭。
用户反馈就是这个作用。你妈不会给你画施工图,但她会告诉你哪里不舒服。
用户的反馈是你产品迭代的方向盘。
不听用户的声音做产品,就像闭着眼睛开车。你可能觉得自己在往对的方向走,但其实已经偏了。
很多成功的独立开发者都说过类似的话:产品的第一个版本不重要,重要的是你能不能根据用户反馈快速迭代。而快速迭代的前提,是你能听到反馈。

三条渠道:邮箱、聊天、社群

收集用户反馈不需要搞得很复杂。对于一个独立开发者的产品来说,三条渠道就够了:
渠道一:邮箱(必须有)
这是最基础的,也是必须有的。
在你产品的页面底部(Footer)放上一个联系邮箱,比如 feedback@你的域名.com 或者 hello@你的域名.com
为什么邮箱是必须的?
1.
它是最低成本的方案——你不需要接入任何第三方服务,有个邮箱就行。
2.
很多正式的反馈适合通过邮件沟通。比如 Bug 报告、功能建议、合作意向等等。
3.
海外用户的习惯。 老外真的爱发邮件,尤其是欧美用户。
怎么做?
很简单,在你产品的 Footer 组件里加上一行:
TypeScript
<p>有问题或建议?联系我们:
  <a href="mailto:feedback@你的域名.com">feedback@你的域名.com</a>
</p>
或者做得更好一点——加一个"联系我们"页面(/contact),放上邮箱和其他联系方式。
小提示: 用上节课学的 Resend 验证过的域名邮箱(比如 feedback@你的域名.com),而不是你的个人 Gmail。这样更专业,也方便你后续管理。
如果你还没有域名邮箱的收件功能,可以用 Cloudflare Email Routing——把发到 feedback@你的域名.com 的邮件自动转发到你的个人邮箱。免费的。
渠道二:在线聊天组件(体验好)
邮箱有个问题:用户需要离开你的产品,打开邮箱,写一封邮件。
这个过程太重了,很多人走到一半就放弃了。
你想想你自己——你在某个网站上遇到问题,你会特地去写封邮件反馈吗?很大概率不会。你心里骂一句,然后直接关掉了。
更好的体验是:用户在你的产品页面上,直接点一下右下角的聊天气泡,就能跟你说话。 不用离开页面,不用打开邮箱,想说什么直接说。
这种东西叫在线聊天组件(Live Chat Widget)。你在很多网站的右下角都见过——一个小气泡图标,点开就是一个聊天窗口。
比如 Creem 右下角的 Get Help ,就是这个形式
我们推荐用 Crisp
为什么选 Crisp?
Crisp 的免费版对独立开发者完全够用:2 个坐席(就你一个人用绰绰有余),基础的即时聊天功能,界面干净好看。
而且 Crisp 接入 Next.js 非常简单——只需要一个环境变量 + 一个组件,就搞定了。
后面实操部分会手把手教你。
渠道三:社群(粘性强)
邮箱是一对一的,聊天组件也是一对一的。但有些时候你需要一个一对多的渠道——让用户之间也能交流,形成社区氛围。
推荐建一个 Discord 服务器
为什么是 Discord?
1.
免费。 创建服务器不花钱,没有人数限制。
2.
海外用户习惯用。 如果你的产品面向海外,Discord 是标配。几乎所有独立开发者的产品都有 Discord 社群。
3.
频道分类清晰。 你可以建不同的频道:#bug-report(报 Bug)、#feature-request(功能建议)、#general(闲聊)、#announcements(官方通知)。
4.
用户之间可以互助。 有人问"怎么用某某功能",不用你自己回答,其他用户可能就帮忙解答了。
如果你的用户主要在国内呢?
那微信群可能更实际。但说实话,微信群有几个硬伤:人数上限(500人)、没有频道分类、历史消息不好搜索。如果你要做一个有一定规模的产品,Discord 还是更好的选择。
怎么做?
1.
创建一个 Discord 服务器。
2.
建好基础频道:#announcements#feedback#bug-report#general
3.
在你的产品里(比如 Footer、设置页面、或者欢迎邮件里)放上 Discord 邀请链接。

实操:给纸片人男友接入 Crisp 在线聊天

三条渠道里,邮箱不用教(加一行 HTML 就行),Discord 不用教(注册后创建服务器就行)。需要教的是 Crisp——因为它要接入你的代码。
好消息是,Crisp 接入 Next.js 极其简单。
第一步:注册 Crisp,拿到 Website ID
1.
打开 Crisp 官网,注册一个账号。
2.
注册过程中它会让你创建一个"Workspace"(工作区),填上你的产品名称。
3.
创建完成后,进入后台,找到 Settings → Workspace Settings → Setup & Integrations
4.
在这个页面上,你会看到一个 Website ID——一串字母和数字。复制它。
第二步:配置环境变量
在项目的 .env.local 文件里加上:
Plain Text
NEXT_PUBLIC_CRISP_WEBSITE_ID=你的WebsiteID
注意这里有 NEXT_PUBLIC_ 前缀——因为 Crisp 的聊天组件需要在前端(浏览器端)加载,所以这个 ID 需要对前端可见。Website ID 本身是公开信息,暴露在前端没有安全问题。
第三步:创建 Crisp 组件
在项目里创建一个组件文件,比如 components/crisp-chat.tsx
TypeScript
'use client'

import { useEffect } from 'react'

export default function CrispChat() {
  useEffect(() => {
    const websiteId = process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID
    if (!websiteId) return

    // 设置 Crisp 的 Website ID
    ;(window as any).$crisp = []
    ;(window as any).CRISP_WEBSITE_ID = websiteId

    // 加载 Crisp 的脚本
    const script = document.createElement('script')
    script.src = 'https://client.crisp.chat/l.js'
    script.async = true
    document.head.appendChild(script)

    return () => {
      // 清理
      document.head.removeChild(script)
    }
  }, [])

  return null  // 这个组件不渲染任何东西,只负责加载 Crisp 脚本
}
这个组件做的事情很简单:页面加载的时候,它往页面里插入 Crisp 的 JavaScript 脚本。这个脚本会在页面右下角显示一个聊天气泡。
第四步:在全局布局中引入
打开你的 app/layout.tsx,把 Crisp 组件加进去:
TypeScript
import CrispChat from '@/components/crisp-chat'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <CrispChat />
      </body>
    </html>
  )
}
放在 layout.tsx 里意味着:你产品的每一个页面都会显示聊天气泡。用户不管在哪个页面,都能随时跟你说话。
第五步:测试
重启项目,打开你的纸片人男友:
1.
右下角是不是出现了一个聊天气泡?
2.
点击气泡,是不是弹出了一个聊天窗口?
3.
发一条消息试试。
4.
打开 Crisp 后台(或者手机上下载 Crisp App)——是不是收到了这条消息?
5.
在 Crisp 后台回复,回到产品页面——是不是看到了回复?
整个过程就像微信聊天一样。用户在产品里发消息,你在 Crisp 后台(或手机 App)回复。

你应该在产品的哪些地方放联系入口?

很多开发者只在 Footer 放了一个邮箱就觉得够了。
但实际上,用户产生反馈冲动的时刻,往往不是在悠闲地翻 Footer 的时候。
他是在用你的产品用到一半、卡住了、炸了、想骂人的时候。那个时候他不会去翻页面底部,他只想找到一个最近的入口一口气吐槽完。
所以,以下这些地方都应该让用户能方便地联系你:
页面底部(Footer):
最基本的位置。放邮箱和 Discord 链接。所有页面都能看到。
产品内"帮助"或"反馈"入口:
在侧边栏或者导航栏加一个"反馈"按钮,点击后弹出 Crisp 聊天窗口或者跳转到反馈页面。
欢迎邮件里:
上节课做的欢迎邮件,里面可以加一句"有任何问题或建议,随时回复这封邮件,或者加入我们的 Discord 社群"。
出错的时候:
如果用户遇到了报错页面(500、404 等),在错误页面上放一个"遇到问题?联系我们"的链接。这是用户最需要帮助的时刻。
用户取消订阅或删除账号的时候:
弹一个简单的表单,问"能告诉我们你为什么离开吗?"。这是最有价值的反馈——因为它直接告诉你"用户为什么不要你了"。

收到反馈后,怎么处理?

收集反馈只是第一步。更重要的是:你怎么处理这些反馈。
第一,快速回复。
不需要马上解决问题,但一定要让用户知道"我看到你的消息了"。
哪怕只是回一句"感谢反馈,我们会尽快看",用户的体验也会好很多。
一个回复及时的独立开发者,会让用户觉得这个产品背后有一个真实的、认真的人。
第二,分类记录。
用户的反馈大概可以分几类:Bug(产品出了问题)、功能建议(希望有新功能)、体验问题(功能有但不好用)、其他(闲聊、合作等)。
你可以用一个简单的表格或者 Notion 文档记录下来,定期回顾。
第三,感谢那些给你提意见的人。
认真告诉他们"你的建议我们采纳了"或者"这个 Bug 已经修复了"。
这些用户很可能变成你最忠实的拥护者——因为他们觉得自己参与了这个产品的成长。
第四,不要试图满足所有人。
你会收到各种各样的建议,有些甚至互相矛盾。不需要全部采纳。
听所有人的意见,但自己做决定。

要点回顾

1.
愿意吐槽你的用户是最有价值的用户。 大部分人不满意就直接走了,连骂都不会骂。你得给他们一个开口的地方。
2.
三条渠道:邮箱(必须)、在线聊天(体验好)、社群(粘性强)。 最低限度,至少放一个邮箱。推荐三条都搭上。
3.
Crisp 在线聊天接入极其简单。 一个环境变量 + 一个组件,每个页面右下角都有聊天入口。
4.
在用户最需要的时刻出现。 Footer、报错页面、欢迎邮件、取消订阅——这些都是放联系入口的好地方。
5.
收到反馈要快速回复、分类记录。 你不需要马上解决问题,但要让用户知道你在听。

课后作业

让你的纸片人男友的用户能找到你。
具体要求:
1.
在产品 Footer 加上你的联系邮箱。
2.
注册 Crisp,接入在线聊天组件——让产品右下角出现聊天气泡。
3.
创建一个 Discord 服务器,建好 #feedback#bug-report#general 三个频道,把邀请链接放到产品里。
4.
在上节课做的欢迎邮件里,加上 Discord 的邀请链接。
5.
(选做)给产品的 404 页面加一个"遇到问题?联系我们"的入口。
提示: 这节课的技术含量不高,但它的价值可能比前面几节课都大。一个有反馈渠道的产品和一个没有反馈渠道的产品,半年后的差距是巨大的——因为前者一直在根据用户声音进化,后者还是半年前的样子。

学习进度确认

你可以点击下方按钮,一键将整门课程标记为学完。

下一篇

【实战进阶(4/4) 】Claude Code、MCP、Skills、产品迭代、开源生态