AI结对编程实战:从零构建单词后台管理系统的完整记录
2026/9/9 1:37:50 网站建设 项目流程

从今年年初开始,我给自己定了一条不成文的规矩:凡是能在 AI 结对编程工具辅助下完成的小项目,就不再从空目录开始死啃文档。上个月刚收尾的“单词后台管理系统”就是这个习惯下最完整的一次样本,技术栈是 Next.js 15(App Router)、Supabase、Drizzle ORM、shadcn/ui。这个项目本身不算复杂,就是一个给外语学习者自己维护单词库、例句和词书的内部后台;真正有意思的是,从头到尾我都没按传统方式“先看文档再写代码”,而是把所有技术问题都变成和 AI 的对话,让它负责产出、我负责验收和纠偏。

这篇文章不是来宣传“AI 已经能替代程序员”的,恰恰相反,我想把过程中 AI 一口气写完的部分、翻车之后我不得不返工的部分,以及我是怎么建立一套可控协作流程的部分都摊开来讲。适合两类读者:一类是刚接触 Next.js 和 Supabase,想找一个真实后台项目练手的人;另一类是已经在用 AI 写代码,但总觉得产出失控、想建立自己协作边界的人。我会把建表、迁移、鉴权、CRUD 页面、排错这些环节的实机操作记录串起来,你可以直接照着走一遍。

1. 先想清楚再动工:我要的到底是“单词后台”还是又一个大而全的 App

1.1 从“背单词”需求里切出“管词库”这一层

很多人一听到“单词系统”,第一反应是做个带每日打卡、遗忘曲线、听力播放的 App。但我在日常学英语时真正的痛点不是没有背单词工具,而是那些工具都在帮你管理“它自己的词库”,我平时从小说、播客、新闻里摘出来的词,散落在备忘录、Excel、笔记软件和手机自带生词本里,没有一个统一的地方能让我把这些词按自己的逻辑组织起来。

这就是我决定做后台系统而不是 App 的原因。后台系统意味着它只关心数据的录入、编辑、组织和检索,不关心用户怎么消费这些数据。它更像一个“仓库管理界面”,先把货架和库存理清楚,未来无论是做网页版复习、手机端卡片,还是导出成 Anki 格式,都只是从这个仓库取数据的问题。

想清楚这一点之后,项目定位就非常清晰:构建一个后台词汇管理系统,核心功能是为个人词库提供灵活的增删改查与组织能力,后续学习应用基于同一数据模型扩展。AI 结对编程在这个阶段的用处很大,它能帮我快速把模糊想法变成需求清单,但前提是我自己得先知道“要什么”。所以我通常会让 AI 以提问方式帮我澄清需求,而不是一上来就让它生成代码。

1.2 MVP 词条范围:先定字段级清单,防止页面做到一半失控

我建议在第一次和 AI 对话前,先自己把最小闭环内的数据字段列一个草稿。这样 AI 生成的代码才有一个明确依据。

我最终的 MVP 字段清单是这样:单词原文、音标、词性、中文释义、例句、例句翻译、来源(比如书名或播客名)、掌握状态、创建时间、更新时间。为什么是这些字段?因为它们覆盖了“录入一个生词必须有的信息”和“之后检索时最想过滤的信息”两大类。比如来源字段,很多单词软件都没有,但对我来说非常重要——我记录一个词的场景直接影响我记不记得住它。状态字段用于标记这个单词处于活用到习惯用法的哪个阶段:待复习、已掌握或已归档。这两个字段不算复杂,但它们决定了后台列表页必须支持“按来源筛选”和“按状态筛选”,数据模型确认得早,后面做页面就不会反复返工。

1.3 把 owner_id 放进去,哪怕单用户也该这么做

早期版本里我想反正只是自己用,就省了用户 ID 字段,所有表都是公共数据。但 AI 提醒了我一个问题:如果未来想把这个单词库开放给朋友一起使用,或者我在手机上开一个端、在电脑上开一个端,两者必须能区分数据归属。更现实的是,我本来就准备用 Supabase Auth 做登录,那数据模型里没有归属字段,RLS(行级安全)就无从生效。后来所有核心表都加上了 owner_id,这个决定让我在“登录后数据隔离”这件事上少走了很多弯路。

2. 技术栈的实际取舍:四件套选型时,AI 说的哪几句话我听进去了

2.1 Next.js 15 给后台管理带来的直接好处:Server Components 和 Server Actions

选 Next.js 做前端不太需要争论。对这个项目来说,最重要的原因是整个后台列表页、详情页基本是“读取数据库后渲染 HTML”,这是 Server Components 最擅长的场景。Drizzle 查询可以直接在服务端执行,不需要像传统前后端分离那样先搭 REST API 再在客户端 fetch。AI 当时给的一句话很关键:你们这个阶段,Server Actions 还没有大量投入,但 Admin 场景下它们非常适合用来替代第一版 REST API。后台管理本质上是内部工具,API 的复用性和开放性不是首要约束,开发速度和修改成本才是。只要界面和数据库之间有服务端这一层,所有数据库操作都在服务端完成,前端只负责收集输入、触发 action,这样我至少省掉了一整套接口文档和维护成本。

2.2 Supabase:Postgres 能力、Auth 和 Docker 本地开发一次到位

Supabase 相当于给你一个托管好的 Postgres,同时附赠了 Auth、Storage 和一套可以随时在浏览器里执行的 SQL 编辑器。对我这种独立开发的小项目来说,单独买一台云数据库或者自己维护 Postgres 实例都不划算,Supabase 免费层足够支撑个人项目。

真正让我推荐它的点是本地开发环境。相关热搜里就有 supabase docker,Supabase CLI 本质上就是通过 Docker 在本地起一整套服务。执行supabase init然后supabase start,它会帮你把 Postgres、Auth、Storage 的镜像全部拉起来,本地跑的数据库和云端的结构完全一致。这样我可以在本地随意测试迁移、折腾表结构,确认无误后再执行与云端数据库的连接和推送,不需要担心把生产环境搞坏。AI 在这个环节能帮我写的通常是 Docker 编排脚本和 SQL 诊断语句,但 Docker 是否在运行、端口有没有冲突这种环境问题,它帮不了你,需要自己看日志。

2.3 Drizzle 和 Prisma 的选择,在“AI 结对”场景下差距比想象中大

数据库工具链我一开始想过 Prisma,毕竟生态成熟、文档多,AI 训练语料里关于 Prisma 的例子也更丰富。但最后选了 Drizzle,原因是 Drizzle 更接近 SQL 本身,生成的客户端更薄,查询都是类型安全的 SQL 构建器,不用经过一层重的运行时引擎。对一个只想管理几千个词条的小项目来说,Prisma 的很多能力用不上,反而每次跑生成都要多一层心智负担。

在 AI 结对这个维度,Drizzle 还有个隐性优点:AI 生成的 Drizzle schema 代码非常贴近“建表 SQL 翻译”,我 review 的时候很容易看出每一个字段映射到什么列;而 Prisma 的 schema 语言和关系映射规则更多,AI 一旦生成多对多关系就容易出现语义偏差。Drizzle 的表定义基本都是函数调用,很直白,适合在对话里一节一节地确认。

2.4 shadcn/ui 的价值不是“组件库”,而是代码长在你项目里

shadcn/ui 和大多数组件库不一样,它不发布 npm 包,而是通过 CLI 把组件源码直接复制到你的项目里。这意味着每个按钮、表格、弹窗的样式和逻辑都是你自己的代码,想怎么改就怎么改。对 AI 结对编程来说,这个特性简直是天然的:AI 可以直接读取组件源码来理解现有的样式变量,而不是去猜某个 npm 包里我们无法看到的内部实现。后期我给表格加行内编辑时,AI 很快定位到components/ui/table.tsx里的样式,这和传统“包在 node_modules 里、AI 看不到”的组件库体验完全不同。

3. 从 0 到 1 搭基础工程:我和 AI 的第一次对话就定好的边界

3.1 可以直接抄走的“第一份提示词”框架

很多人在 AI 编程工具里第一步就问“帮我搭建一个 Next.js 项目”,这种提问方式通常只会得到一段泛泛而谈的步骤,因为 AI 既不知道你的环境,也不知道你的验收标准。我的经验是:第一份提示词必须包含目标、技术约束、上下文、输出格式四个部分。下面这个模板你可以直接改成自己的项目来用:

目标:帮我规划一个“单词后台管理系统”的工程结构,技术栈已经确定: Next.js 15(App Router)+ TypeScript + Tailwind CSS + shadcn/ui + Supabase(Postgres + Auth)+ Drizzle ORM。 技术约束: - 所有数据库访问必须封装在独立的>pnpm dlx create-next-app@latest word-admin \ --typescript \ --tailwind \ --eslint \ --app \ --src-dir \ --import-alias "@/*"

跑完之后的第一件事,不是急着写业务代码,而是把生成的package.jsontsconfig.json原封不动地贴给 AI,告诉它“这是我们当前的版本基线,所有依赖判断都以这份 package.json 为准”。这一步能避免后面很多莫名其妙的版本问题。

3.3 目录脚手架:先约法三章,再让 AI 动手

初始化完成后,我会和 AI 一起确定下面的目录结构:

src/ app/ (admin)/ words/ page.tsx # 单词列表页 new/page.tsx # 新建单词页 dashboard/page.tsx # 首页概览 layout.tsx login/page.tsx components/ ui/ # shadcn/ui 组件 words/ # 单词模块的业务组件 db/ schema.ts # Drizzle 表定义 index.ts # 数据库连接 lib/ supabase/ client.ts # 浏览器端客户端 server.ts # 服务端客户端 server/ actions/ words.ts # 单词相关的 Server Actions

这个结构的核心原则是:数据库访问独立,页面组件独立,Server Actions 独立。AI 在生成代码时如果遵循这个结构,后期维护会非常顺;如果 AI 想“简化”掉>import { pgTable, uuid, text, timestamp, integer, } from "drizzle-orm/pg-core"; export const wordStatus = ["learning", "mastered", "archived"] as const; export const words = pgTable("words", { id: uuid("id").defaultRandom().primaryKey(), word: text("word").notNull(), phonetic: text("phonetic"), partOfSpeech: text("part_of_speech"), definition: text("definition").notNull(), exampleSentence: text("example_sentence"), exampleTranslation: text("example_translation"), source: text("source"), status: text("status", { enum: wordStatus }) .notNull() .default("learning"), ownerId: text("owner_id").notNull(), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }) .defaultNow() .notNull(), });

这里有一个和 AI 协作时的经典陷阱:word语义其实是一个“词条”,不是“单词”这个简单概念。如果你只定义字段不管约束,AI 可能不设置notNull,也可能忘掉给释义设置非空。我让 AI 同时生成一个 zod 校验 schema,把表单层和数据库层的约束统一起来,省的后面写校验时再翻一遍数据库定义。

4.2 词书与词条的多对多:让 AI 明确 onDelete 语义

光有单词表还不足以体现“管理系统”的价值,我还需要能按“主题词书”来组织词条,比如“《人类简史》词汇”“口语高频词”“法律英语词”。一个词条可以同时出现在多个词书里,所以需要一张连接表。

这里我特意让 AI 把删除策略列出来,而不是让它自己猜。Drizzle 关联关系里,把wordbook_entries里的外部键设置为级联删除相对合理:

export const wordbookEntries = pgTable("wordbook_entries", { id: uuid("id").defaultRandom().primaryKey(), wordId: uuid("word_id") .notNull() .references(() => words.id, { onDelete: "cascade" }), wordbookId: uuid("wordbook_id") .notNull() .references(() => wordbooks.id, { onDelete: "cascade" }), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), });

为什么需要和 AI 单独确认?因为 AI 默认生成的onDelete行为往往是no action,也就是删词条的时候如果它挂在某本词书下,数据库会直接报错而不是自动清理关联。对于后台管理系统来说,用户删除一个词,预期就是这个词从所有词书里消失,而不是弹出一个外键约束错误。如果你忘了定义级联,后面删除接口会变成 bug 高发区。

4.3 drizzle-kit 迁移:generate 和 push 的顺序别搞反

表结构定义好之后,接下来要生成迁移文件并应用到本地 Supabase。我的工作流是:

npx drizzle-kit generate npx drizzle-kit migrate

generate会根据 schema 文件的变化生成带时间戳的 SQL 迁移文件,migrate会把这些 SQL 应用到当前连接的数据库。这里容易遇到的问题就是顺序:有些教程会让你直接db push,它确实更省事,但不留历史记录。多人协作或需要回滚时,没有迁移文件会非常被动。对于个人项目,虽然 push 也够用,但我还是倾向于让它生成迁移 SQL,我会肉眼 review 一遍drizzle/目录里的 SQL 文件再执行,久而久之就养成了检查迁移的习惯。

AI 在这一步的参与方式很特别:它不直接执行命令,而是帮我检查生成的 SQL。我会把迁移文件内容贴给它看,问它“这个迁移有没有可能破坏已有数据?”,AI 通常能指出ALTER COLUMN ... SET NOT NULL在这张表已有数据时可能会失败之类的问题。虽然机器不是万能的,但这种“AI 做代码 review + 我做最终决策”的方式效率很高。

4.4 关于搜索,别在一开始就上重量级方案

在保留的核心字段之外,有一个优化字段我在 MVP 阶段选择不加,即全文搜索索引。单词表的搜索场景其实是前缀匹配,比如用户输入 “abo” 希望匹配到 “aboard”“abominable”。Postgres 的ILIKE加前缀索引其实就够用了。我自己验证过,几千行记录的表在这种场景下查询时间可以忽略不计。如果之后单词量到十万级再考虑 pg_trgm 或者单独接搜索服务也不迟。数据模型设计最重要的原则是别为想象中的未来买单,先让它服务好 MVP 功能。

5. CRUD 页面怎么从 AI 草稿变成能用的后台:表格、弹窗与表单

5.1 列表页的做法:页面服务端取数,表格只做展示

后台系统最常见的页面就是“表格 + 搜索 + 分页”。我的做法是让列表页本身是一个 Server Component,负责接收 URL 参数、调>// src/app/(admin)/words/page.tsx import { findWordsByPage } from "@/server/data-access/words"; import { WordsTable } from "@/components/words/words-table"; export default async function WordsPage({ searchParams, }: { searchParams: Promise<{ page?: string; q?: string; status?: string }>; }) { const { page = "1", q = "", status = "" } = await searchParams; const { items, total } = await findWordsByPage({ page: Number(page), q, status, }); return ( <div className="space-y-4"> <WordsTable items={items} total={total} q={q} status={status} /> </div> ); }

这个写法有几个好处:第一,URL 天然是当前状态的序列化,刷新页面后仍然停留在当前页和关键词,不用额外维护全局状态;第二,服务端查询不需要额外的 loading 状态,首屏 HTML 直接包含数据,打开后台的感受非常快。AI 在这个环节非常擅长生成findWordsByPage这类查询函数,但你要盯住的是它在>"use server"; import { createServerSupabaseClient } from "@/lib/supabase/server"; import { deleteWordById } from "@/server/data-access/words"; import { revalidatePath } from "next/cache"; export async function deleteWordAction(formData: FormData) { const wordId = formData.get("id"); if (typeof wordId !== "string") { throw new Error("invalid id"); } const supabase = await createServerSupabaseClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) { throw new Error("请先登录"); } await deleteWordById(wordId, user.id); revalidatePath("/words"); }

为什么权限判断放这里而不是放在按钮上?因为按钮隐藏只是前端体验,真正要防的是有人直接构造请求调用 Server Action。Server Action 虽然叫“服务端函数”,但它本质上是可以从客户端触发的端点,所以它内部必须自己校验身份。AI 经常忽略这个要点,因为它生成的代码看起来“能点能删”,但你一旦把页面做成客户端组件就必须考虑这个问题。

5.4 形态控制:为什么不让 AI 把整个页面变成一个巨型客户端组件

第一次让 AI 生成列表页时,它倾向于把整个页面都标记成客户端组件,把所有数据通过 fetch 拿到客户端再渲染。这样做的结果就是:搜索一次打一次 API,页面还要自己管理 loading 和 error 状态,代码量凭空多出三分之一。

后来我要求它遵循一个默认规则:页面级别的数据获取尽量放在 Server Component,只有按钮点击、弹窗开合、输入框受控这些交互片段才是客户端范围。shadcn/ui 的表格本身其实没有“逻辑”,它只是样式组件,可以在服务端组件里直接引用。真正需要use client的是表格内的操作按钮(删除确认弹窗、编辑按钮跳转或弹窗组件)。这样的组合在 Next.js 里才能发挥 Server Components 的效率。

6. Supabase Auth 与 RLS:“登录能过但查不到数据”的完整排查链路

6.1 从老的 supabase-js 用法换到 @supabase/ssr 的必经改造

如果你以前搜索过“Next.js Supabase 登录”,大概率看到的教程还停留在用@supabase/supabase-jscreateClient直接初始化,然后把 session 存到 localStorage 里。这套方案在 App Router 下已经不推荐了,更稳妥的做法是使用@supabase/ssr这个包来创建服务端客户端和浏览器端客户端。

服务端侧的核心代码大致是这样:

import { createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; export async function createServerSupabaseClient() { const cookieStore = await cookies(); return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll(); }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ); } catch {} }, }, } ); }

所谓“从 localStorage 改为 cookie”,核心差异是:服务端组件读取 session 时不需要等待浏览器端状态,每次请求都能在服务端完成认证判断。AI 最大的问题在于它经常给出新旧两种 API 混用的代码,要么用了getSession()这个已被标记过时的接口,要么手动处理 cookie 的存取逻辑。后面第 7 节我会专门展开这个坑。

6.2 RLS 策略:让 AI 写策略,但你必须亲自 review 的三种写法

Supabase 的 Postgres 默认对数据表打开 RLS,也就意味着如果你不写任何 policy,任何外部请求都查不到数据,哪怕你已经通过 Auth 登录也白搭。AI 写 RLS 策略经常出现三种错误:一是策略忘了加to authenticated,二是usingwith check混用,三是把硬编码的用户 ID 写进去。

我最终的 policy 非常简单直接:

create policy "words_owner_all" on public.words for all to authenticated using (auth.uid()::text = owner_id) with check (auth.uid()::text = owner_id);

注意这里owner_id我在 Drizzle schema 里定义为 text 类型,方便和auth.uid()的返回值比较。如果要更严谨,可以用 uuid 类型并建立外键到auth.users。但跨 schema 引用 auth 表有时会让开发期迁移变得麻烦,这个取舍取决于你想在数据库层做到多严格。

6.3 排查“空列表”的完整步骤:别总怀疑是你查询写错了

实际开发里我遇到过很多次“页面显示空白表格,但数据库里明明有数据”的情况。如果你的应用也出现类似现象,建议按下面的顺序排查:

  1. 确认当前登录用户能在 Supabase 的 Authentication 页面里看到 session。
  2. 到 Supabase Dashboard 的 SQL Editor 里执行select auth.uid();,确认当前请求上下文能拿到用户。
  3. 在本地用supabase db dumpselect * from words;看一下数据是否存在,且owner_id和上面拿到的auth.uid()是否匹配。
  4. 回到 policy 页面,确认 RLS 是开启状态且 policy 的using条件与 SQL 一致。

90% 的“登录但无数据”问题,最后都出在owner_id没写对或 RLS 没开。这个过程如果让 AI 参与,你可以直接把报错信息和表结构一起贴给它,让它列出假设,但你仍然要自己做 SQL 验证。毕竟 AI 看不到你的真实数据库内容,它只能猜。

7. 这次结对编程翻车最狠的 4 个坑,以及我的纠偏方式

7.1 现象一:AI 生成了旧版 Auth API,一运行就报错

我的第一次翻车发生得非常早,AI 在生成登录页面时生成了类似下面的代码:

const supabase = createClient(url, key); const { data, error } = await supabase.auth.signInWithPassword({ email, password, });

光看这段没什么问题,但它前面调用createClient的路径和配套的 cookie 管理方式完全基于旧版@supabase/supabase-js,并没有用@supabase/ssr。当我把它集成到服务端布局里时,报错明确指出cookies()在服务端组件的调用方式已经改变。这个坑的本质是 AI 的训练数据里含有大量旧教程内容。纠正方式也比较简单:我把package.json中相关依赖的版本号贴给 AI,并要求它“只用 package.json 中存在的包来引用代码,不引入 package.json 之外的任何依赖”。这样它就不会再生成@supabase/supabase-js的旧写法了。

7.2 现象二:shadcn/ui 初始化和 Tailwind 版本互不匹配

shadcn/ui 在项目里的安装方式比较特殊,它会把组件源码复制到components/ui目录中。如果你用的是 Tailwind v3,而 AI 生成的组件样式是基于 v4 的写法,默认情况下页面会出现大量样式丢失甚至编译失败。那次的问题出现在 AI 为了“省事”,让我在 Tailwind v4 项目里直接执行npx shadcn@latest add button并手工改了一堆配置,结果很多类名对不上。

这个坑的规避方法是:不要手工混着来,直接用 shadcn CLI 初始化,让 CLI 帮你判断当前 Tailwind 版本。

npx shadcn@latest init

AI 结对编程里最忌讳的是让 AI“顺手帮你改配置文件”。配置文件之间的依赖关系非常隐晦,比如tailwind.config.tspostcss.config.mjsapp/globals.csscomponents.json这些文件是耦合的。AI 往往只看到你贴给它的单个文件,不了解全套配置。我的经验是:涉及配置文件修改时,把相关文件全部发给 AI,并且让 AI 在一开始列出它将改动哪些文件,我确认后再动手,绝不让它跨文件“自由发挥”。

7.3 现象三:AI 在 Server Action 里直接信任客户端传参

Server Action 的入参本质上来自客户端请求,因此必须有和开放 API 一样的校验意识。AI 在生成编辑单词的 action 时,一开始是直接把整个表单对象传进来然后更新数据库,完全没有检查这个单词的 ownerId 是不是当前登录用户。这意味着只要我知道某个单词的 id,就可以伪造请求改成别人的词条。这个 bug 在 RLS 层面其实会被拦截,因为更新时 Drizzle 生成的 SQL 会先执行 policy 检查,所以最终数据库层面还算安全,但代码里这种信任客户端的坏味道必须在作用层面就杀干净。

我给 AI 的补丁指令是:在updateWordAction的函数体第一屏加上三件事,一是身份校验,二是入参的 zod 解析,三是确认受影响的词条归属当前用户。只有都通过才继续执行更新,更新完再revalidatePath。把这三件事作为所有 action 的标准模板,后面再生成其他 action 时,我会在 prompt 里带上这个模板,让 AI 复用。

7.4 现象四:AI 自己捏造了不存在的表结构和“假”数据

最让我哭笑不得的一次是 AI 在一个页面里引用了word_tags表,并自动生成了几个关联查询。但我整个 schema 里根本没有这张表,那是它在生成代码时“补全”出来的。为什么会出现这种情况?因为 AI 在大段生成时会基于它对“一个单词系统应该有标签”的常识来补全模型,而不是严格基于我们前面定义好的 schema。

避免这个问题的办法是:在生成业务代码前,先把完整的 Drizzle schema 文件粘贴给 AI,并写一句“所有数据库和字段引用只能从上面这个 schema 文件中取,如果不存在请直接说明,不要自己补全”。这句话能显著降低虚构表出现的概率。另外,我每让 AI 生成完一批文件,都会执行一遍类型检查和少数几个针对性查询来验证。在 AI 结对的工作流里,持续编译是最重要的“谎言探测器”。

7.5 我现在和 AI 结对的最小工作流

经历了这些翻车之后,我现在已经形成了一套比较固定的工作流,分享出来可以参考:

  1. 需求阶段:我口述,AI 提问澄清,产出字段清单和页面清单。
  2. 工程阶段:AI 给命令,我执行,产出package.json基线。
  3. 数据阶段:AI 按字段清单生成 Drizzle schema,我 review 后执行 generate + migrate。
  4. 功能阶段:按“查询 → action → 页面 → 组件”的顺序,一次只让 AI 改一个文件或一个功能。
  5. 验证阶段:每完成一个小循环都跑 typecheck + lint + build。
  6. 回溯阶段:如果报错,把完整错误信息发给 AI,并附上相关文件内容,禁止它只猜测。

这套流程的核心是:AI 负责产生候选方案,我负责设边界、验收、回滚。它不是结对编程里“人人平等”的关系,更像是高级开发者和实习开发者的关系,实习生速度快、体力好,但仍然需要一个经验更丰富的人在旁边盯着。

8. 跑通核心闭环之后,我接下来接的功能与用法扩展

8.1 批量导词:JSON 导入 + 按字词去重

核心 CRUD 跑通之后,后台系统才能真的开始积累词库。下一步我给项目加了“导入词库”功能:用户粘贴一段 JSON,系统批量解析并写入数据库。

const parsed = batchWordSchema.parse(JSON.parse(rawJson)); for (const item of parsed) { await db .insert(words) .values({ ...item, ownerId: user.id, }) .onConflictDoNothing(); }

onConflictDoNothing需要表上有唯一约束。这里我加上了一个基于(owner_id, word)的联合唯一索引,语义是“每个用户同一个单词只存一条”。这个约束让后续无论是导入还是手动输入都不用担心产生重复脏数据。AI 在这个功能上表现得很好,因为导入解析、去重逻辑相对独立,且容易写单元测试。

8.2 从“管理词库”延伸到“按词书学习”

当后台单词量积累到一定程度,我自然会在前台做一个简单的“按词书练习”页面。这个页面的接口逻辑其实完全是现成的:查词书 ID、返回该词书下所有词条、按复习间隔做筛选。因为当初做的就是干净的数据结构分离,前台页面甚至不需要改数据库。那时你会感谢自己在 MVP 阶段没有图省事把所有数据塞在一张 blob 表里。

8.3 我现在的真实使用习惯和总结

这个系统上线后,我实际使用中的感受是:最初两天热度很高,把多年积累的几百个词条都录了进去;中期开始变得懒散,因为录入一个词需要填的字段确实偏多。于是我又加了一个“快捷录入”模式,只需要填单词和释义,其他字段后面有需要再补。这个改动让我意识到,个人后台类工具最重要的是降低录入摩擦,而不是功能多。如果一开始就要求所有字段必填,再好的工具也会被闲置。

回看这次项目,最大的体会是“AI 结对编程”真正提升的不是写码速度,而是你敢于从 0 启动一个项目的心理门槛。以前我会因为要查一堆文档而拖延,现在只需要把目标拆得足够细,然后像布置任务一样安排给 AI。要注意的是,AI 永远只会给你一个“看起来合理的答案”,它不会为你的数据安全、字段语义和长期维护负责,这些还是要靠人来把握。如果你也想做类似的小系统,我建议从今天开始选一个你反反复复想做的项目,把第一份提示词写出来,剩下的,就是反复对话、执行和打磨的过程了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询