袁锐钦 · AI编程实践者
Day 1 把项目骨架和 CSS 洗衣机搞定了。
但现在的 Picboil 还是个玩具——谁都能用,没有用户概念,生成过的图片没地方存,免费次数没法控制。
Day 2 要干的事很明确:让玩具变成一个能用的产品。
三件事:
做网站需要登录,选项一大堆:
方案 | 优点 | 缺点 |
|---|---|---|
Clerk | 开箱即用,UI漂亮 | 免费额度少,按量计费贵 |
自己写 JWT | 完全可控 | 安全细节多,容易出漏洞 |
NextAuth.js v5 | 开源免费 + Next.js 原生集成 | 需要自己配 |
我选了 NextAuth.js v5(现在也叫 Auth.js)。
理由很实际:开源免费、Next.js 生态原生装上就能用、支持邮箱密码 + Google 登录等多种方式。MVP 阶段每一分钱都要算。
提示词模板: 如果你也要让 AI 帮你搭认证系统,可以这样下指令: "用 NextAuth.js v5 配置认证系统,支持 Credentials Provider(邮箱+密码)和 Google OAuth Provider,使用 JWT Session 策略。给出完整的配置代码和路由文件。"
两种登录方式:
① 邮箱 + 密码登录
最传统的方式。用户填邮箱、设密码,密码用 bcrypt 哈希后存到数据库。没什么花哨的,就是稳。
② Google 一键登录
海外用户特别吃这一套——老外看到 "Continue with Google" 就放心了,不用记密码。
③ JWT Token 存登录状态
用户登录后发一张 JWT 卡片,下次访问自动验证身份。好处是部署简单(不用在服务器存 session),坏处是注销没那么即时。MVP 够用了。
AI 帮我把这套配通之后测试了一下——邮箱注册 ✅,Google 登录 ✅,退出再进 ✅。
看起来没问题。
然后问题来了。
选了 Prisma ORM + PostgreSQL。
什么是 ORM? 简单说就是"翻译官"——你用 JavaScript 写代码,ORM 帮你翻译成数据库能听懂的 SQL 语句。不用手写 SQL,不容易出错。
为什么选 Prisma?它是 Next.js 生态里最成熟的 ORM,AI 写 Prisma 代码也最顺手,文档全、社区大。
设计了 4 张表:
users → 用户信息(邮箱、密码哈希、名字)
credits → 积分余额(每天免费几次、已用几次、付费余额)
tasks → 生成任务记录(提示词、参数、状态)
images → 生成的图片记录(图片地址、大小、关联任务)一个关键设计决策:软删除模式。
每张表都加了 deleted_at 字段。删除数据不是真删,只是打个时间戳标记"这条已删除"。
为什么?
AI 写好 schema,npx prisma migrate dev 一跑,PostgreSQL 里 4 张表整整齐齐建好了。
看起来也没问题。
然后问题真的来了。
前面都是铺垫,这里才是干货。
现象: 配置完启动项目报错,提示数据库连接字符串有问题。明明 .env 文件里写了连接地址,但运行时读不到。
原因: AI 默认给我装了 Prisma v7(最新版)。但 v7 有很多破坏性更新,其中之一就是废弃了旧版的数据库配置方式,改成运行时动态获取。结果各种不兼容。
怎么修:
# 直接降级到稳定版
npm install prisma@5 @prisma/client@5不是我不想用新版,而是 MVP 阶段我的优先级是能跑,不是追新。v5 稳定、文档全、AI 写代码也靠谱。等以后产品稳定了再考虑升级。
💡 提示词技巧: 让 AI 装依赖时,可以加一句"使用稳定版本而非最新版",或者直接指定版本号如 "使用 prisma@5"。能省掉很多兼容性坑。
教训:AI 总爱装最新版,但最新版不一定最稳定。锁定版本很重要。
这个最有意思,值得展开讲。
现象: 我设置了每个用户每天免费生成 5 张图。但快速连点两次生成按钮,有时候能用 6 次、甚至 7 次。
为什么?
这是一个经典的竞态条件(Race Condition)。
通俗解释: 想象一个只有1个窗口的票务处。两个人同时冲进去买最后一张票。工作人员 A 查了一下"还有1张",工作人员 B 也查了"还有1张",然后两人都把票卖出去了。多卖了1张,航空公司亏了。
我们的系统也一样:
请求A(第一次点击) | 请求B(第二次点击) |
|---|---|
读数据:已用 4 次 | 读数据:已用 4 次 |
判断:4 < 5,没超限 ✅ | 判断:4 < 5,没超限 ✅ |
+1 → 变成 5 次 | +1 → 变成 6 次! |
两个请求同时读到 4,都判断没超限,都加了 1。用户白嫖了一次。
如果这是付费积分呢?用户本来只有 10 个积分,并发请求可能只扣了 1 次的钱就生成了 2 次。直接亏钱。
怎么修:
用数据库的原子操作——让"读取并加1"变成不可分割的一步:
// ❌ AI 写的原始代码(有 bug)
const user = await prisma.user.findUnique({ where: { email } });
if (user.free_used_today < 5) {
await prisma.user.update({
where: { email },
data: { free_used_today: user.free_used_today + 1 }
});
}
// ✅ 修复后(原子操作,不会出问题)
await prisma.$transaction([
prisma.credits.update({
where: { userId },
data: { free_used_today: { increment: 1 } }
})
]);{ increment: 1 } 是 Prisma 的原子操作符。数据库保证这个操作不可分割,两个请求进来也会排队处理,一个一个来。
💡 提示词技巧: 涉及积分/余额/库存等数值操作时,在 prompt 里加一句"注意处理并发安全,使用数据库原子操作或事务"。AI 默认写的代码不考虑并发,因为它默认世界是串行的——但真实用户会连点、会刷新、会同时开多个标签页。
教训:任何涉及"先读再改"的操作都要考虑并发。
现象: 先用邮箱注册了一个账号(有免费积分),然后用同一个 Google 账号登录——结果系统把它当成了新用户,或者更糟,积分没了。
原因: AI 写的认证逻辑只处理了两种情况:
但它漏掉了第三种情况:
用户先用邮箱注册了,后来改用 Google 登录。邮箱是同一个,但登录方式不同(一个是邮箱密码,一个是 Google)。系统该怎么处理?
AI 的原始代码在 Google 回调里只查了 where: { email, provider: 'google' }。如果这个用户之前是用邮箱注册的,Google 登录时就查不到他——要么报错,要么给他建了个重复账户,积分归零。
怎么修:
登录时先查所有方式下有没有这个邮箱,有就直接放行,没有才创建新的:
// 不只查 google provider,查所有
const existingUser = await prisma.user.findFirst({
where: { email: googleEmail } // ← 不限定 provider
});
if (existingUser) {
// 老用户换了一种登录方式,直接放行
return existingUser;
} else {
// 真的新用户,创建 + 发积分
return await createUser(googleEmail, 'google');
}💡 提示词技巧: 让 AI 写认证逻辑时,加上"处理多 provider 账号合并场景:同一邮箱通过不同方式注册/登录时,应关联到同一账户而非创建重复账户"。
教训:认证系统的边界情况比主流程还多。正常流程能跑通不代表没问题——你得想:换一种登录方式会怎样?先登 A 再登 B 会怎样?过期了再来会怎样?
现象: 用户输入 "hello" (根本没有 @ 符号),系统拿去加密存储,白白消耗算力。
为什么这是个问题:
怎么修:
加一行格式校验,不合法的直接拦回去:
// 先校验格式,通过再走加密
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('邮箱格式不正确');
}
// 校验通过才走 bcrypt
const hashedPassword = await bcrypt.hash(password, 12);💡 提示词技巧: 在注册/登录相关需求里加一条"所有用户输入必须先做格式校验再进行后续处理(bcrypt/数据库写入/API调用)"。这几乎是免费的防护,但 AI 经常忘记做。
教训:先验证再处理,别让无效数据进入流水线。 这跟工厂生产一样——原料不合格就不该送进车间,不然产出的全是废品,还得花成本返工。
现象: 代码里写着各种孤零零的 5:
if (user.free_used_today >= 5) return error;
// ...
const DAILY_LIMIT = 5;
// ...
<span>剩余 {5 - user.free_used_today} 次</span>今天我记得 5 是"每天免费生成次数"。但下周呢?三个月后呢?或者别的同事来看这段代码呢?
他们只会看到一个孤零零的 5,不知道这是什么意思。如果想改成每天 3 次呢?得搜遍整个代码库把所有 5 改掉——万一有些 5 不是指免费次数呢?改错了就出新 bug。
怎么修:
抽成常量,一处定义全局引用:
// constants.ts
export const DEFAULT_FREE_DAILY = 5;
// 所有地方统一用常量
if (user.free_used_today >= DEFAULT_FREE_DAILY)
const remaining = DEFAULT_FREE_DAILY - user.free_used_today💡 提示词技巧: "代码中所有业务相关的数字必须定义为命名常量(如 FREE_DAILY_LIMIT),禁止出现 magic number。" 一句话就能避免这个问题。
教训:魔法数字是维护噩梦的开始。 今天你觉得"这很明显是什么意思",三个月后你会感谢当初写了注释的自己。
这部分今天没有太多可讲的——在 Stripe 官网注册了账号,拿到了 API 测试密钥。
作为出海站,支付是必须的。Picboil 的商业模式是 Freemium(免费增值):免费用户每天有限额,付费用户解锁更多功能和更高额度。
Stripe 接入留到后面再做,今天先把账号和基础环境搞定。
✅ NextAuth.js v5 认证系统(邮箱 + Google OAuth) ✅ Prisma + PostgreSQL 数据库(4张表,软删除模式) ✅ 积分系统的原子操作修复(并发安全) ✅ Google OAuth 多账户关联修复 ✅ 邮箱格式前置校验 ✅ 魔法数字常量化 ✅ Stripe 注册 + 测试环境配置
项目 | 花费 |
|---|---|
时间 | 约 8 小时 |
金钱 | $0(全部用免费额度 / 开源方案) |
新增依赖 | NextAuth + Prisma + PostgreSQL + bcrypt |
修复 Bug 数 | 5 个 |
Day 1 教会我的是"不要过度工程化",Day 2 教会我的是"AI 写的代码不能直接信"。
5 个 Bug 里,有 3 个是 AI 写代码时的"典型毛病":
AI 能帮你写出能跑的代码,但能不能在生产环境稳定运行,取决于你自己有没有审查意识。
我不是程序员出身,但我正在学会一件事:
不要对着 AI 生成的代码说"看起来没问题",而要问自己"如果我是用户,我会怎么搞崩它"。
从这个角度思考,今天这 5 个 Bug 每一个都有价值——因为它们都是在上线前发现的,总比上线后被用户发现好。
到今天这篇文章的时候,实际上 Day 3 的活已经干掉大半了:
✅ 滚筒转动与 AI 生成的衔接 — 动画结束 → 调 Gemini API → 图片出来
✅ 生成历史展示 — 用户能看到之前生成过的图
🔲 便签纸飞进洗衣机的动画序列 — 从输入到生成的完整交互链路(待完善)
三天下来,Picboil 已经是一个能完整走通"注册→登录→输入→生成→查看"流程的最小可用产品了。
下一步就是打磨交互体验,然后准备上线测试。
如果你在做 AI 辅助开发,或者对这些踩坑经历有共鸣,欢迎交流。
一个人走得快,一群人走得远。