最近我用 Claude Code 从零搭了一个宠物生命周期管理 App从工程初始化、数据库设计、后端接口到前端页面和部署几乎全程通过对话式编程完成。整个过程跑下来最大的感受是AI 编程真正改变的已经不是“能不能写代码”而是“你如何把需求拆到它可以稳定执行的颗粒度”。这篇文章就把完整流程复盘一遍包括我怎么拆 MVP、怎么设计宠物数据模型、怎么让 Claude Code 生成提醒逻辑、以及联调和部署时那些不会写进官方文档的坑。适合想尝试 AI 辅助开发完整项目的人也适合准备做宠物类工具产品的开发者参考。我尽量用实操细节而不是空话很多指令我直接把当时发给 Claude Code 的提示词整理出来了你可以照着用也可以换成自己的项目重跑一遍。需要说明的是我并不是把全部工作丢给 AI 就不管了我会在关键节点做 review、写测试、修正边界情况。这才是 AI 辅助开发新项目时真正能落地的姿态。1. 动手之前把“宠物生命周期管理”拆成能执行的 MVP1.1 这类 App 的核心价值到底在哪宠物生命周期管理不是简单的“记备忘录”。宠物从幼年、成年到老年每个阶段都有不同的照护重点幼年需要频繁打疫苗和驱虫成年需要年度体检和体重监测老年可能涉及慢性病、牙齿、关节等问题。市面上通用日历工具能提醒时间但它不知道怎么区分疫苗和驱虫也不会根据宠物体重趋势给出生理上的预警。所以这个 App 的核心价值是三点结构化记录、阶段化提醒、趋势可视化。我选择这个方向还有一个原因它足够复杂却不失控。它要处理多个实体之间的关联关系比如一只宠物有多条疫苗记录、多个体重记录、多个健康事件它还需要一个能根据出生日期动态计算生命周期阶段的逻辑。这些刚好是测试 AI 编程能力的理想场景又不至于像做电商系统那样要撑起一大堆用户权限和订单状态机。1.2 和 Claude Code 敲定的 MVP 边界第一次和 Claude Code 对话时我没有直接说“帮我做一个宠物 App”而是给了非常明确的边界单用户本地优先不做登录注册不建社区、不做商城、不做在线问诊做五个核心功能宠物档案、疫苗与驱虫记录、体重记录、健康事件时间轴、近期提醒看板。把边界写清楚很重要。Claude Code 这类工具很擅长“顺着用户意图延展”你只说一句“宠物管理 App”它大概率会给你生成用户系统、多宠物共享、社交动态、消息通知等一堆超范围功能。我当时的办法是每条指令前都加一句“当前阶段不涉及多用户、不做社交”然后在项目根目录写一份 CLAUDE.md把需求边界持久化让后续每一轮对话都能看到。1.3 为什么把“生命周期阶段”设计成动态计算一开始我让 Claude Code 直接在数据库里给 Pet 表加了一个 stage 字段存“幼年/成年/老年”的字符串。后来 review 时发现这不对宠物只要过了生日它的阶段就会变如果你把 stage 落库就还要写定时任务去批量更新或者等用户打开 App 时才更新非常容易过期。更好的做法是不存 stage而是根据 species 和 birthDate 实时计算。这个判断逻辑放在 service 层宠物详情接口返回时带上 lifeStage 字段。前端拿到直接展示不需要关心计算过程。这个决定让整个项目省掉了大量“数据维护”工作也让我确认了一个原则AI 生成的代码里凡是涉及时间、状态、业务规则的部分都要多问一句“这个值应该存进去还是算出来”。2. 技术栈选型与项目初始化2.1 怎么选一套 AI 友好的技术栈我最后选的是 React TypeScript Vite 做前端Express Prisma SQLite 做后端部署时再把数据库切换到 PostgreSQL。选这套方案不是因为它们最炫而是因为它们生态成熟、训练语料多Claude Code 生成这些框架的代码时准确率明显更高。其中几个关键选择值得展开说TypeScript 是必选项它能给 AI 生成的数据结构提供类型约束接口字段错了 IDE 和编译阶段就能发现Prisma 作为 ORM用 Schema 文件描述模型AI 可以直接修改模型、执行迁移比手写 SQL 更适合对话式开发先用 SQLite 提速线上换 PostgreSQL。这样本地不需要额外装数据库服务等部署时才引入完整数据库能力。如果你是在做一个非常小的 Demo用 Next.js 全栈也可以少维护两个服务。但我这次想验证一条“可复用的新项目流水线”前后端分离仍然是多数项目的主旋律所以我保留了前后端两个目录。2.2 初始化工程并让 AI 生成项目骨架前端用 Vite 官方脚手架初始化npm create vitelatest pet-life-frontend -- --template react-ts cd pet-life-frontend npm install后端独立目录初始化mkdir pet-life-backend cd pet-life-backend npm init -y npm install express cors dotenv npm install -D typescript tsx types/express types/node初始化这种机械操作我没让 AI 代劳因为它已经足够快了而且脚手架版本更新频繁直接用 CLI 更稳。真正交给 Claude Code 的是初始化完成后的目录设计。我在它的交互终端里给出这样一段话这是一个宠物生命周期管理 App 的后端服务技术栈为 Express TypeScript Prisma。请按 routes、controllers、services、utils、middlewares 分层创建目录并在 README 中说明如何启动开发服务。它很快生成了目录结构和基础 README但我也注意到一个老问题如果在一个会话里同时要求“创建目录”和“实现所有文件”它会生成许多互相引用但不存在的模块。所以我的经验是拆成两步先只生成骨架再逐个文件让 AI 填内容。2.3 项目约定文件比提示词更重要我想强调一个很容易被忽略的点和 Claude Code 合作前先准备一份清晰的 CLAUDE.md。它不是普通项目文档而是 AI 的“长期记忆”。每一轮新会话开始前工具都能看到它。我写的内容大致是文件命名统一用 kebab-caseAPI 返回格式统一为{ data }或{ error }日期一律使用 ISO 8601 字符串所有需要人工确认的 TODO 必须留在代码里并标注。这些约定看起来很琐碎但实际效果很大。没有这些约束时AI 可能在一个项目里同时生成 camelCase 和 PascalCase前端接口字段和老后端命名不一致联调时来回改。有了 CLAUDE.md 之后我明显感觉到生成的代码风格稳定了review 成本低了很多。3. 数据模型设计让 AI 打地基但验收必须靠自己3.1 核心实体与关系设计宠物生命周期管理的关键数据模型我最终拆成了这几张表model Pet { id String id default(cuid()) name String species String // DOG 或 CAT breed String? birthDate DateTime gender String // MALE 或 FEMALE neutered Boolean default(false) avatarUrl String? createdAt DateTime default(now()) vaccines VaccinationRecord[] dewormings DewormingRecord[] weights WeightRecord[] healthEvents HealthEvent[] } model VaccinationRecord { id String id default(cuid()) petId String vaccineName String vaccinatedAt DateTime nextDueDate DateTime? clinic String? notes String? pet Pet relation(fields: [petId], references: [id], onDelete: Cascade) } model WeightRecord { id String id default(cuid()) petId String date DateTime default(now()) weightKg Float pet Pet relation(fields: [petId], references: [id], onDelete: Cascade) }第一版 AI 把疫苗、驱虫、健康事件都塞进 Pet 的 JSON 数组里虽然也能用但后续查询提醒会非常痛苦。我要求它按“每种记录一张表”来拆并且所有外键都加上onDelete: Cascade。这样删除宠物档案时不会留下孤儿数据。这个决策背后的逻辑是业务记录需要独立查询、独立排序、独立设置到期时间。如果塞进 JSONSQL 查询很难优雅地筛出“未来 30 天疫苗到期的记录”。实体拆得越清晰AI 在后续生成查询逻辑时就越不容易犯糊涂。3.2 SQLite 下枚举字段的坑第一次 Claude Code 生成的 Schema 里用了enum Species { DOG CAT }看上去没问题但 Prisma SQLite 不支持枚举字段执行迁移直接报错。这个坑很典型AI 学了很多 PostgreSQL 实践但不一定会主动适配 SQLite。解决办法是改成字符串并在应用层做校验export const PET_SPECIES [DOG, CAT] as const; export type PetSpecies typeof PET_SPECIES[number];同时在 controller 里增加一层参数校验不是白名单里的值直接返回 400。这个方案在 SQLite 和 PostgreSQL 上都能跑避免了迁移环节的兼容性问题。3.3 迁移和 Seed 数据的生成数据建模完成后就是执行迁移npx prisma migrate dev --name init为了让前端联调有数据可用我还让 Claude Code 生成了一份 seed 脚本自动创建三只虚拟宠物分别处于幼年、成年、老年阶段各配几条疫苗和体重记录。这个做法非常推荐因为 AI 生成的页面一开始需要真实数据才能看到图表和提醒效果而没有 seed 数据只能手动在数据库里插效率太低。我在生成种子数据时加了一条重要约束所有日期要相对当前日期动态生成不能写死。否则过两周数据就全部过期提醒功能看起来永远为空。这一点是 AI 容易犯的错它通常会写new Date(2024-01-01)而不是now - 某段时间。4. 后端 API 与生命周期提醒的核心逻辑4.1 API 分层与接口清单后端最终维护的接口不多但每个接口背后都有清晰的职责分层。我没有让路由回调里直接写业务逻辑而是按 routes、controllers、services 三层拆开方法路径说明GET/api/pets宠物列表附带生命阶段POST/api/pets新建宠物档案GET/api/pets/:id宠物详情返回最新体重POST/api/pets/:id/vaccinations添加疫苗记录GET/api/pets/:id/timeline健康事件倒序时间轴GET/api/reminders近 30 天疫苗和驱虫提醒聚合为什么不直接让 AI 把逻辑都写在 router 里因为一旦逻辑复杂AI 生成的函数会越来越长后续你很难只针对某一块做修改。分层之后每个文件的职责非常单一AI 生成和修复的命中率都会提高。我在给 Claude Code 的指令里明确写了“路由层只解析参数controller 层处理 HTTP 响应service 层只放业务逻辑”它基本能按这个规则执行。4.2 生命周期阶段的计算逻辑宠物详情和列表接口都需要返回lifeStage。我的 service 里有一个核心函数export type LifeStage PUPPY | KITTEN | ADULT | SENIOR; export function getLifeStage( species: DOG | CAT, birthDate: Date ): LifeStage { const ageMonth (Date.now() - new Date(birthDate).getTime()) / (1000 * 60 * 60 * 24 * 30.44); if (species DOG) { if (ageMonth 12) return PUPPY; if (ageMonth 96) return ADULT; return SENIOR; } if (ageMonth 12) return KITTEN; if (ageMonth 132) return ADULT; return SENIOR; }这里有个很容易被忽略的点狗的老年标准其实和体型强相关小型犬 10 岁左右进入老年大型犬可能 6 岁就进入了。我当前版本为了保持 MVP 简单只按品种粗略划分并在注释里标明“简化版本”。如果你真的要做面向兽医的产品应该在 Pet 模型里增加 size 字段再按体型去查对应参考表。这种专业领域知识AI 不会自动替你想到需要人工补充。我还让 Claude Code 帮这个函数生成了单元测试边界条件包含出生当天、刚满 1 岁、临近 8 岁、刚满 11 岁。这一步很有价值因为日期计算最容易出现“差一个月”或者“时区偏移一天”的问题。测试跑出来的结果确实发现过一次 bugAI 用月份数比对时把ageMonth 96和ageMonth 8混用过导致成年犬直接进入老年。这种问题靠人工看可能也能发现但测试能更快锁定。4.3 未来 30 天提醒聚合提醒看板的逻辑是每次请求/api/reminders时查询所有下次执行日期落在“当前时间”和“当前时间 30 天”之间的疫苗记录与驱虫记录再按日期升序返回。对应 Prisma 查询大致是const now new Date(); const soon new Date(); soon.setDate(soon.getDate() 30); const vaccines await prisma.vaccinationRecord.findMany({ where: { nextDueDate: { gte: now, lte: soon, }, }, include: { pet: true }, orderBy: { nextDueDate: asc }, });这个模块还有一个边界让我和 AI 来回折腾了几次如果某条疫苗记录没设置nextDueDate就不应该出现在提醒列表里。AI 第一版直接把所有未来日期都查出来NULL 值也被带进去导致前端渲染日期为空。后来我在提示词里增加“query 条件必须排除 nextDueDate 为 null 的记录”再配合类型定义才彻底解决。这个经验也说明和 AI 沟通时业务边界条件必须一条条说清楚。AI 能听懂“排除空值”但如果你没说它基于常规偏好会把所有记录返回给前端由前端再去判断。5. 前端核心页面与交互实现5.1 路由与组件规划前端我使用 react-router-dom 的 BrowserRouter规划了四个主要页面/dashboard查看近期提醒和宠物概览/pets宠物列表/pets/new新建宠物档案/pets/:petId宠物详情含时间轴和体重图。组件层面拆成了 PetCard、LifeStageBadge、WeightChart、ReminderList、HealthTimeline。这个拆分也是让 Claude Code 生成的但我的指令里明确要求“每个组件文件一个默认导出组件只接收必要 props不要在组件内部直接发请求”。这保证了组件可复用、易测试不会变成一坨耦合在页面里的代码。5.2 让 AI 生成宠物卡片组件我给 Claude Code 的提示词是这样的生成 PetCard 组件接收 pet 对象和 onClick 回调。展示头像、名字、品种、生命阶段徽章。如果 avatarUrl 为空用默认占位图。它生成的代码骨架我简单调整后如下interface PetCardProps { pet: Pet; onClick: (petId: string) void; } export function PetCard({ pet, onClick }: PetCardProps) { return ( div classNamerounded-xl border p-4 shadow-sm cursor-pointer hover:shadow-md transition onClick{() onClick(pet.id)} img src{pet.avatarUrl ?? /placeholder.png} alt{pet.name} classNameh-16 w-16 rounded-full object-cover / h3 classNamemt-2 text-lg font-semibold{pet.name}/h3 p classNametext-sm text-gray-500{pet.breed ?? 未知品种}/p LifeStageBadge stage{pet.lifeStage} / /div ); }这种组件生成率很高但我会特别注意两件事一是 pet 的类型定义是否和后端返回的 DTO 字段一致二是 onClick 参数是否传的是 id 而不是整个事件对象。AI 偶尔会把onClick{() onClick(event)}这种奇怪写法带进来所以 review 是必须的。5.3 数据获取与状态管理前端数据请求我用了 tanstack/react-query它让缓存、重试、加载态管理都变得简单。API 调用我封装在一个 api.ts 文件里例如const BASE_URL import.meta.env.VITE_API_BASE_URL ?? /api; export async function getPets(): PromisePet[] { const res await fetch(${BASE_URL}/pets); if (!res.ok) throw new Error(加载宠物列表失败); return res.json(); }在页面里配合 useQuery 使用const { data, isLoading, error } useQuery({ queryKey: [pets], queryFn: getPets, });这里最容易犯的错误是新增宠物后列表不刷新。因为 react-query 默认会缓存查询结果你需要在新宠物 mutation 成功后调用invalidateQueries([pets])。Claude Code 第一版没写这个我新增宠物后列表一直是旧的。后来我在提示词里的通用约定写上了“每次 mutation 后必须 invalidate 对应的 queryKey”这个问题就消失了。5.4 体重折线图的生成体重趋势用的 recharts 折线图。AI 生成图表本身很快但有三个细节要人工盯着X 轴数据必须先按日期升序排序否则折线会乱日期格式化要用toLocaleDateString否则横轴会挤满时间戳Y 轴最好设置 domain 为 auto 区间否则体重轻微变化会显得波动很大。核心代码结构如下ResponsiveContainer width100% height{200} LineChart data{weightPoints} XAxis dataKeydate tickFormatter{(value: string) new Date(value).toLocaleDateString() } / YAxis domain{[dataMin - 1, dataMax 1]} / Tooltip / Line typemonotone dataKeyweightKg stroke#4f46e5 / /LineChart /ResponsiveContainer这个组件我前后让 Claude Code 改了三次。前两次生成的版本数据没排序曲线会来回穿越第三次我直接告诉它“数据源要在传入 chart 前完成 date 排序”它才输出正确的结构。这说明 AI 不会替你想业务合理性问题你必须在关键逻辑上把关。5.5 表单校验与提交新建宠物表单我用了 react-hook-form配合 zod 做一个轻量校验。必填项包括名字、物种、出生日期性别和绝育状态用默认值。表单提交成功后跳回宠物详情页。AI 生成的表单通常只有最基础的提交逻辑不会处理“日期不能在未来”“物种必须是 DOG/CAT”这类业务校验。我在给 Claude Code 的提示词里专门加了一条“出生日期超过当前日期时给出生动错误提示”。它对这种单点约束处理得不错所以我建议把校验规则拆成一句话一句话地下达不要让它一口气想象出所有规则。6. 联调、构建与部署过程中踩过的真实问题6.1 本地联调代理配置前端开发服务器默认跑在 5173 端口后端 Express 跑在 3000 端口。直接发请求必然跨域。我在 Vite 配置里加代理server: { proxy: { /api: http://localhost:3000 } }这样前端代码里所有/api请求都自动转发到后端不需要在后端开启 CORS 也能联调。如果是在生产环境前后端分开部署才需要后端配置 CORS 白名单。本地联调阶段代理的方式最省心。6.2 从 SQLite 切到 PostgreSQL 的注意事项本地开发一直用的 SQLite部署前需要切到 PostgreSQL。这一步不只是改个 DATABASE_URL 那么简单。Prisma schema 如果包含 SQLite 不支持的类型切库时会报错我们已经用字符串代替枚举所以没遇到太大阻力。迁移命令也有区别。本地开发用prisma migrate dev它会重置数据库并应用迁移线上环境用prisma migrate deploy它只执行未应用的迁移不会重置已有数据。我在部署前踩过一个坑第一次直接用了migrate dev执行线上环境导致数据库被重置。后来改为 deploy 才对。前端构建时我也遇到了 TypeScript 类型报错。AI 生成的代码里经常有未使用的变量或者不匹配的类型定义本地 dev 不会拦截但vite build因为配置了tsc -b会直接失败。解决办法很简单让 Claude Code 查看报错信息并修复一次然后把“构建前必须通过 tsc”写进 CLAUDE.md。6.3 高频问题速查表问题原因解决办法Prisma migrate 报枚举不支持SQLite 不支持 enum改用字符串 应用层校验提醒日期比本地时间早 8 小时日期存储为 UTC后端统一返回 ISO 字符串前端本地格式化刷新页面后 404前端路由回退未配置生产环境把所有请求回退到 index.html新增宠物后列表不更新React Query 缓存未失效mutation 成功后调用 invalidateQueries构建时疑似类型错误AI 生成了多余导入或错误类型跑tsc -b根据报错逐项修复提醒列表出现空日期nextDueDate 为 NULL 也被查出查询条件显式排除 null这张表几乎覆盖了我这次项目里遇到的大部分问题。很多问题都不是“代码写不出来”而是环境差异、状态管理和边界条件导致的。这类问题你让 AI 单看报错信息也能修但最好自己理解原因否则它改一次可能又会引入新的副作用。7. 连续用 Claude Code 开发后我沉淀下来的几条习惯7.1 任务拆得越小产出质量越高我早期问过“帮我实现宠物后台的全部接口”Claude Code 一次生成了很多文件结果一半接口字段没对齐另一半依赖不存在。后来我改成“先实现 GET /api/pets 列表接口包含 lifeStage 计算”一次只做一件事生成质量明显稳定。AI 编程很像带实习生你可以给它一个高难任务但它最有把握的永远是小而明确的任务。这个颗粒度大约是一个文件、一个接口、一个组件的程度。7.2 关键业务逻辑必须人工 review所有涉及日期计算、状态流转、金钱或安全的部分我会逐行看一遍。Claude Code 能写出结构漂亮的代码但它对业务规则的理解完全来自你的提示词提示词没说清楚它就按最通用的逻辑去写。比如生命周期阶段如果我不给体型参数它会默认所有狗 8 岁进入老年。你需要在业务层面做决策再把这个决策告诉它。7.3 测试不是用来证明 AI 正确而是用来兜底AI 生成的代码跑一遍测试就能通过不代表正确测试用例本身也需要人工设计。我给 getLifeStage 设计的边界测试就是自己先想的然后让 AI 把用例补全。这样做的好处是等后面改代码时至少有一条防线能提醒你“年龄计算逻辑被改坏了”。没有测试的 AI 辅助开发等于没有安全网的高空作业能省时间但风险太高。最后再分享一个小技巧如果你也准备用 Claude Code 从零搭项目第一次跑通后再回过去重构不要指望一次生成所有代码把它当成一个随时在线的结对程序员需求说清、边界立好、关键逻辑亲自确认你的项目才能真正落地。