第 6 章 · 前端、后端与数据库怎么协作
解剖「添加一个单词」这条最细链路:页面、请求、服务端处理、数据保存、结果反馈各做了什么;数据结构、登录身份与访问权限的最小讲解。
这一章解决什么问题
第 5 章你跟着任务清单让 AI 做出了「添加一个单词」,但代码是 AI 写的——如果不理解这条链路,第 7 章出问题时你连「坏在哪一段」都问不出来。这一章把 T2 那条最细的全链路解剖开,回答三个问题:一个词是怎么从输入框进入数据库的?网站怎么知道「是你」在操作?凭什么别人看不到你的词?
本章所有材料按「能不能亲手跑」分了级,最后有一张对照表。你不需要在本章写出完整应用——目标是看懂链路,为排错装上地图。
开始前你要有什么
- 第 5 章 T2 完成的项目(边读边对照自己的代码,效果最好);没有的话,纯读也成立
- 本地装了 sqlite3(Mac 自带;终端输入
sqlite3 --version有输出即可) - 想通一个前提:前端与后端和数据存在哪两篇概念课讲过的基础,本章直接用
分工:人判断什么,AI 做什么
本章是理解章,没有要 AI 替你做的活。判断在你:读到每一段,你都该能回答「这一段坏了,页面上会看到什么」——这是下一章排错的核心能力。AI 的用法是答疑:哪段代码看不懂,圈出来让它逐句解释。
怎么做
第 1 步:先看全景——一条词的七段旅程
你在表单里点「保存」,到新词出现在列表里,中间发生这些事:
浏览器(前端) 服务器(后端) 数据库
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ ① 表单收集输入 │ │ │ │ │
│ ② 发送请求 ──────┼──────────────▶ ③ 验明身份 │ │ │
│ │ HTTP │ (会话→user_id)│ │ │
│ │ │ ④ 校验内容 │ │ │
│ │ │ (空?太长?) │ │ │
│ │ │ ⑤ 写入 ───────────┼──▶ │ ⑥ INSERT │
│ │ │ │ │ 存一行 │
│ ⑦ 收到响应, │◀─────────────┼── 返回结果 ───────┼─── │ │
│ 刷新列表 │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────┘记住这个分工:前端管收集和展示,后端管校验和决定,数据库管保存。第 7 章的排错地图就是它——「点了没反应」查①②,「提示保存了但刷新就没」查⑤⑥,「别人看得到我的词」查③。
第 2 步:数据长什么样——words 表
PRD 数据需求一节落到数据库里,就是一张表(Postgres 方言,与第 4 章选的 Supabase 一致):
create table words (
id bigint generated always as identity primary key,
user_id uuid not null, -- 这行词归哪个账号(登录服务提供)
word text not null, -- 单词,如 resume
meaning text not null, -- 释义(必填:强迫第一次加工)
example text, -- 例句,选填
source text, -- 在哪遇到的,选填
status text not null default 'learning', -- learning / mastered
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (user_id, word) -- 同一账号不重复(AC6 的数据库层兜底)
);四个设计决定,每个都对应前面章节的一个 D 或 AC:
- user_id 是必填的外来字段(D6):每一行都记着「我属于谁」,它是权限的地基——后面查词永远带着它;
- status 只有两个固定值(D3):状态切换就是把这一列从 learning 改成 mastered,筛选就是把等于 learning 的挑出来;
- unique (user_id, word)(AC6):重复录入不靠前端提醒,数据库本身拒绝——最后一道防线;
- 两个时间戳:看似多余,第 7 章排查「这条到底存没存上」时,created_at 就是你去找它的线索。
第 3 步:亲手跑一遍(零账号、零注册,约 15 分钟)
不需要 Supabase 账号——Mac 自带的 sqlite3 就能把「归属、查重、切换、筛选」四个核心逻辑跑通。把下面整段粘贴进终端:
sqlite3 /tmp/vocab-sandbox.db-- 建表(SQLite 方言;与上面 Postgres 版的差异只在自增主键和时间的写法)
create table words (
id integer primary key autoincrement,
user_id text not null,
word text not null,
meaning text not null,
status text not null default 'learning',
unique (user_id, word)
);
-- 两个账号各录一条
insert into words (user_id, word, meaning) values
('user-A', 'resume', 'v. 重新开始;n. 简历'),
('user-B', 'negotiate', 'v. 谈判');
-- ① 账号 A 的词表(权限查询的雏形:永远带 user_id 条件)
select word, status from words where user_id = 'user-A';预期输出(本教程编写时实际执行的输出):
resume|learning继续:
-- ② 账号 A 重复录入 resume(AC6 会发生什么)
insert into words (user_id, word, meaning) values ('user-A', 'resume', '重复的释义');Error: stepping, UNIQUE constraint failed: words.user_id, words.word (19)数据库直接拒绝——这就是 AC6「最后一道防线」的真实样子。前端提示只是友好外衣,拦截靠它。
-- ③ 标记已掌握(AC4 的本质是 update)
update words set status = 'mastered' where user_id = 'user-A' and word = 'resume';
select word, status from words where user_id = 'user-A';
-- ④ 筛选未掌握(AC5 的本质是带条件的 select)
select word from words where user_id = 'user-A' and status = 'learning';
-- ⑤ 权限反例:如果查询忘了带 user_id 条件,会发生什么?
select user_id, word from words;第④步预期输出只剩 deliver 一类 learning 状态的词(按你录的内容而定);第⑤步是本章最重要的一眼:没有 user_id 条件,user-B 的 negotiate 也会被查出来——这就是 AC8 要防的事故。区别只在一句 where。
顺手说一个诚实声明:上面输出是本教程编写时在 sqlite3 3.51 真实执行的结果;你的执行环境不同,输出细节(比如错误编号)可能略有差异,但拦截、过滤、归属的行为不会变。玩完了删掉沙盒:
rm /tmp/vocab-sandbox.db。
第 4 步:服务端校验长什么样——一段真实跑过的代码
后端第④步「校验内容」不是一句空话,它就是这样一个函数(本教程编写时用 node 实际执行过 6 组测试,全部通过):
// 校验录入内容:通过返回干净的数据,不通过返回中文错误
export function validateWord(input: unknown): { ok: true; value: WordInput } | { ok: false; error: string } {
const { word, meaning, example } = (input ?? {}) as Partial<WordInput>;
if (typeof word !== 'string' || word.trim().length === 0) {
return { ok: false, error: '单词不能为空' };
}
if (word.trim().length > 100) {
return { ok: false, error: '单词最长 100 个字符' };
}
if (typeof meaning !== 'string' || meaning.trim().length === 0) {
return { ok: false, error: '释义不能为空——用你自己的话写' };
}
// ……长度上限检查同型,略
return {
ok: true,
value: { word: word.trim(), meaning: meaning.trim() },
};
}注意错误文案是写给人看的(「用你自己的话写」),不是写给程序的。这段代码跑在服务端——就算有人绕过页面直接发请求(第 7 章权限演练里你就这么干),校验依然生效。只在前端校验 = 没校验。
第 5 步:把七段旅程接成代码(示例节选)
把链路接起来的服务端代码,形如(Next.js route handler 风格的示例节选,用于讲解——本教程没有为它搭建完整项目运行,属于示例代码;你的项目里对应的代码由模板和第 5 章任务生成,细节必然不同,但骨架一致):
// POST /api/words —— 「添加一个单词」的服务端(节选,讲解用)
export async function POST(request: Request) {
// ③ 验明身份:从会话里取当前用户(见下节「登录身份」)
const user = await getUserFromSession();
if (!user) {
return Response.json({ error: '未登录' }, { status: 401 });
}
// ④ 校验内容
const body = await request.json();
const check = validateWord(body);
if (!check.ok) {
return Response.json({ error: check.error }, { status: 400 });
}
// ⑤ 写入数据库(user_id 来自会话,不由请求提供——防伪造)
await db.insert(words).values({
user_id: user.id,
word: check.value.word,
meaning: check.value.meaning,
});
// ⑦ 告诉前端成了
return Response.json({ ok: true }, { status: 201 });
}三个标注出来的点,是全链路的安全骨架:
- user_id 从会话取,不从请求取:请求体是任何人都能伪造的;会话才是可信身份。
- 校验不过就返回 400:把失败明确说出口(对照反模式被吞掉的报错)。
- insert 失败会抛错:数据库层 unique 冲突时这里会炸,正确做法是接住它翻译成「已录入过」(AC6 的友好版)——第 5 章任务 T5 会让 AI 补这块。
第 6 步:登录身份与访问权限——两个大白话概念
登录身份(会话):你登录成功后,登录服务发给你一张「通行证」(技术上是一小段存在浏览器 cookie 里的凭证),之后每个请求自动带上它,服务端验一下就知道「是你」。所以:换浏览器 = 没带通行证 = 要重新登录(AC1);登出 = 销毁通行证。
访问权限:通行证证明「你是 user-A」,但查哪些数据仍由服务端决定。原则一句话:
隐藏入口不等于权限。 菜单不显示别人的词、链接不给别人,都只是「不给人看路牌」;只要查询不带 user_id 条件,懂行的人直接请求接口照样全量拿走。权限必须落在服务端查询里(第 3 步沙盒第⑤眼看过的那种差别)。
AC8 的测法因此很明确:开第二个浏览器注册 user-B,不是「看看界面上有没有 A 的词」,而是直接请求列表接口——界面可以骗人,接口的响应骗不了人。
产物与检查
本章产物是你的链路理解。自测三题(合上教程回答):
- 点「保存」后提示成功、刷新却没了——七段旅程里哪几段最可疑?
- 为什么 user_id 必须从会话取,不能从表单或请求里取?
- 「别人看不到我的词」靠的是界面不显示,还是别的什么?AC8 怎么测?
(答案分别在第 7 章演练一、本章第 5 步、本章第 6 步。三题都答对,本章毕业。)
三样东西,别混为一谈
本章和整篇教程里出现过三种「数据/代码」,性质完全不同:
| 名称 | 是什么 | 在本教程哪里 | 能当真吗 |
|---|---|---|---|
| 模拟数据 | 编出来讲故事的假内容 | 第 1 章「300 多条备忘录」、决策树的示例回答 | 不能——全部是教学假设 |
| 示例代码 | 讲解用的节选片段 | 本章第 5 步的 route handler | 结构可信;本教程没为它搭完整项目,逐行照抄到你项目里未必能跑 |
| 真实实现 | 你项目里实际运行的代码 | 你按第 5 章任务做出来的项目 | 唯一算数的东西;对照本章理解它、排错它 |
判断口诀:讲故事的一律是模拟,贴出来讲解的是示例,你自己项目里跑起来的才是真的。
不满意或失败时怎么调整
- SQL 沙盒跑不出预期输出:检查是不是整段粘贴(
create table和insert必须先执行);还是不行就删掉/tmp/vocab-sandbox.db从头再来——沙盒的乐趣就是随便砸。 - route handler 节选取代了你项目里的真实代码:不要抄。打开你项目里对应的文件(问 AI「我的添加单词接口在哪个文件」),对照本章骨架找那三个安全点在不在。缺了哪个点,就是第 7 章的隐患。
- 七段旅程记不住:别背,记住分工口诀就够——前端收集展示、后端校验决定、数据库保存。
交给下一章什么
- 七段旅程图——第 7 章排错地图:每个现象先定位到段落,再谈修复;
- 沙盒的直觉——「数据真的存进去了吗」从此可以自己验证,不靠界面撒谎;
- user_id / 会话 / 服务端过滤——第 7 章权限演练的理论底子。
案例进度:T2 的链路已解剖。下一章,我们按 PRD 逐条验收,并故意布下三个故障当靶子练排错。