最近我用 Claude Code 完整搭了一个宠物生命周期管理 App,从项目初始化、数据库建模、后端接口、前端页面,到最后部署脚本,几乎没有手写过整段代码。整个过程跑下来,最大的感受是:AI 编码工具的能力边界已经不只是补全几行函数,而是可以扮演整个研发链条里的“主力执行者”,真正需要你操心的是需求定义、边界控制和方案取舍。
这篇文章不打算做成工具宣传,而是想完整记录我这次从零到一的全流程实操。标题里的“生命周期”是个容易被低估的词,宠物不是静态的物品档案,它背后有幼年、成年、老年的阶段判断,有疫苗计划、体重曲线、就诊历史、营养提醒。这些业务规则叠在一起,复杂度刚好够让 AI 正常发挥,又不会大到失控。如果你正打算在真实项目里尝试 Claude Code,或者想做一个宠物类产品但不太熟悉工程化,这篇可以直接照着走。
我用的技术栈是 Node.js + TypeScript + Express + Prisma + React + Vite + TailwindCSS,开发环境数据库用 SQLite,生产环境切成 PostgreSQL。没有选冷门框架,因为 Claude Code 对主流技术栈的训练数据最充足,克制选择反而能得到更稳定的生成结果。
1. 项目定位:宠物生命周期管理到底要管什么
1.1 需求拆解:不能做成一个“宠物记账本”
拿到题目后,我没有急着让 Claude Code 写代码,而是先让它进入“产品分析模式”。我给的初始指令是:
我要做一个宠物生命周期管理 App,覆盖宠物从领养/出生到离世的全部关键节点。 先不要写代码,帮我列出这个产品需要哪些核心业务域和数据实体。这个指令的关键在于“先不要写代码”。Claude Code 默认会倾向直接生成工程文件,如果把需求分析和代码生成混在一次对话里,输出会变得非常发散。结果它返回了五个业务域:宠物档案、免疫计划、体重管理、健康记录、生命周期事件。我用表格把骨架固化下来:
| 业务域 | 核心数据 | 典型场景 |
|---|---|---|
| 宠物档案 | 名字、品种、生日、性别、绝育状态、照片 | 查看当前年龄与生命阶段 |
| 免疫计划 | 疫苗名称、接种日期、加强针间隔、状态 | 到期前推送提醒 |
| 体重管理 | 体重数值、测量时间、变化趋势 | 判断是否超重或过瘦 |
| 健康记录 | 就诊时间、诊断说明、用药清单 | 复诊时翻历史记录 |
| 生命周期事件 | 出生时间、离世时间、关键阶段变化 | 自动展示幼年/成年/老年 |
这一步非常重要。很多 AI 辅助开发翻车,不是因为 AI 不会写代码,而是需求边界没定清楚就让 AI 自由发挥。后面让 Claude Code 基于这张表生成数据库字段时,它给出的结构就明显收敛了许多,没有出现“用户表里塞宠物字段”这种混乱设计。
1.2 为什么把“生命阶段”当作产品的核心概念
宠物生命周期 App 和普通进销存系统的本质区别,是它天然存在“状态随时间变化”的逻辑。一只狗在 1 岁前是幼犬,7 岁以后可能进入老年期,猫和狗的标准还不一样。如果这些阶段被做成数据库里的固定字段,那用户一旦发现生日填错了、或者领养日修正了,所有历史统计都会跟着出错。
所以我在一开始就给项目定了一个技术原则:生命阶段不落库,用出生日期实时计算。这个决策贯穿了后面所有的接口和前端组件,也直接影响了 Claude Code 生成的代码风格。它会在查询宠物列表时把“年龄天数”和“阶段标签”一起返回,而不是存一个写死的stage字段。
另外我刻意把业务复杂度控制在一个能落地的范围,比如疫苗提醒规则、体重趋势计算、基础 CRUD 是必须项,但支付、多宠物家庭权限、社交分享这些先不做。交给 AI 的工具一旦需求太宽,产出质量会断崖式下降,这是我在多个项目里反复验证过的经验。
2. 环境准备与项目骨架:第一次给 Claude Code 下指令要说清五件事
2.1 工具链准备与启动
我本机已经装了 Node.js 20 和 pnpm,Claude Code 通过命令行安装后,在项目目录里直接启动就行。这里我建议创建全新空目录,不要让 AI 去读一个已经存在很久的仓库,否则历史代码会严重干扰生成结果。
node -v pnpm -v claude --version mkdir pet-life-app && cd pet-life-app claude第一次启动会要求授权工作目录读写权限,确认后就能进入交互式终端。我在这个终端里做的第一件事不是生成代码,而是给整个项目写了一份“初始任务说明”。
2.2 一个有效初始指令的完整示例
下面是我实际用过的初始指令,它比一句“帮我搭个项目”有用得多:
# Role 你是一名全栈工程师,负责从零搭建一个宠物生命周期管理 Web App。 # 项目背景 - 产品面向宠物主,记录宠物从领养到离世的完整健康时间线。 - 技术栈固定为:Node.js + TypeScript + Express + Prisma + React + Vite + TailwindCSS。 - 开发环境数据库用 SQLite,生产环境可迁到 PostgreSQL,查询语句尽量标准。 - 需要同时包含后端 API 和前端页面。 # 第一步任务 1. 创建 monorepo 目录结构,区分 server 和 web。 2. 初始化 TypeScript 配置、ESLint 和基础脚本。 3. 暂不编写业务代码,只把工程骨架立起来。这条指令的价值在于五个明确:角色明确、背景明确、技术边界明确、任务范围明确、验收目标明确。如果不约束“只把工程骨架立起来”,Claude Code 很可能一口气生成几十个业务文件,到时候删起来比从零写还痛苦。
Claude Code 生成的目录结构大概是这样的:
pet-life-app/ ├── package.json ├── pnpm-workspace.yaml ├── server/ │ ├── package.json │ ├── tsconfig.json │ ├── prisma/ │ └── src/ │ ├── index.ts │ └── routes/ └── web/ ├── package.json ├── vite.config.ts ├── index.html └── src/ ├── main.tsx └── App.tsx2.3 骨架生成后的第一轮人工清理
AI 生成的骨架通常会有“过度设计”的毛病。这次它额外生成了一个docs/目录,里面放了几篇看起来很像样的接口规范文档,但内容其实是通用的模板示例,和宠物业务没有直接关系。我直接把这些模板文件删掉了,只保留真实需要的部分。
另一个细节是版本锁定。Claude Code 写入 package.json 的依赖版本往往不是最新,但组合可能是自洽的。如果你手动升级某个包,很容易触发依赖地狱。我的建议是:生成完骨架后先整体跑一遍pnpm install,然后进server和web分别执行一次构建脚本,确认基础链路能跑通,再开始加业务。
3. 数据模型设计:把“一只宠物的一生”翻译成数据库结构
3.1 从需求表到 Prisma Schema
我把上一阶段确定好的五张核心表格直接丢给 Claude Code,要求它生成 Prisma Schema。Prisma 的好处是模型关系清晰,AI 生成迁移也比较顺手。核心模型简化后是下面这样的:
model Pet { id String @id @default(cuid()) name String species Species breed String? gender Gender birthDate DateTime photoUrl String? weightRecords WeightRecord[] vaccinationLogs VaccinationLog[] healthRecords HealthRecord[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model WeightRecord { id String @id @default(cuid()) petId String pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade) weightKg Float measuredAt DateTime } model VaccinationLog { id String @id @default(cuid()) petId String pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade) vaccineName String givenAt DateTime nextDueAt DateTime? } model HealthRecord { id String @id @default(cuid()) petId String pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade) title String diagnosis String? happenedAt DateTime } enum Species { DOG CAT } enum Gender { MALE FEMALE }实际生成时还包含了 User 表、Reminder 表和通知设置表,这里为了篇幅做了裁剪。从建模结果看,Claude Code 能比较好地理解“宠物主拥有多个宠物,宠物拥有多个健康事件”的关系,并且自动加了外键约束和级联删除。这比很多新手手写外键要稳健得多。
3.2 生命阶段计算逻辑:不落库,按日期实时算
这是我整个项目里最看重的一段代码。我要求 Claude Code 写一个独立的类型和工具函数,不依赖数据库字段。
export type LifeStage = 'PUPPY' | 'ADULT' | 'SENIOR'; export function getLifeStage( birthDate: Date, species: 'DOG' | 'CAT', ): LifeStage { const ageMonths = (Date.now() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30); if (species === 'DOG') { if (ageMonths < 12) return 'PUPPY'; if (ageMonths < 84) return 'ADULT'; // 7岁左右 return 'SENIOR'; } if (ageMonths < 12) return 'PUPPY'; if (ageMonths < 132) return 'ADULT'; // 11岁左右 return 'SENIOR'; }当时我给 Claude Code 的补充指令是:
生命阶段不要存库,统一放在 server/src/utils/lifeStage.ts。 狗和猫的成年/老年标准不一样,函数需要接收 species 参数。 所有列表接口返回 pet 时,同时输出 ageDays 和 lifeStage。这里有一个值得展开的工程决策:如果直接在查询列表里对每只宠物调用getLifeStage,数据量上来后性能会有浪费。但宠物管理 App 的真实体量通常不会大到需要为这种计算建立缓存或冗余字段。违背直觉的是,Claude Code 过于“聪明”时会主动建议引入 Redis 缓存或物化视图,对这种规模的项目就是过度设计。我的处理是让这个计算函数保持为纯函数,方便测试,同时前端卡片组件里也复用同一个函数。
3.3 AI 建表过程中的三个常见坑
第一坑是枚举值风格混乱。Claude Code 可能在一次对话里生成dog、DOG、犬三种写法,必须提前约束统一用大写英文枚举。
第二坑是时间字段语义不清晰。它可能把“下次疫苗日期”直接落成一个固定Date,但业务上这个日期是由疫苗规则计算出来的,不应该写入主表。要在建模指令里写明“nextDueAt是冗余计算结果,不作为唯一事实来源”。
第三坑是缺少onDelete: Cascade。没有级联删除时,删除一只宠物会报外键错误,真实用户操作直接白屏。让 AI 生成模型后,最好手动把所有@relation检查一遍。
4. 后端 API 的业务逻辑实现:疫苗提醒和生命周期筛选
4.1 标准化 CRUD 接口的生成方式
Claude Code 生成 CRUD 接口的熟练度非常高,但默认生成的代码往往风格散漫,有的返回{ data: pet },有的直接返回pet。我让它先抽一个统一响应包装函数:
export function ok<T>(data: T) { return { code: 0, data, message: 'ok' }; } export function fail(message: string, code = 1) { return { code, data: null, message }; }然后所有路由都基于这个函数收口。比如获取宠物详情:
router.get('/pets/:id', async (req, res) => { const pet = await prisma.pet.findUnique({ where: { id: req.params.id }, include: { weightRecords: { orderBy: { measuredAt: 'desc' } }, vaccinationLogs: { orderBy: { givenAt: 'desc' } }, }, }); if (!pet) { res.status(404).json(fail('宠物不存在')); return; } res.json(ok({ ...pet, ageDays: getAgeDays(pet.birthDate), lifeStage: getLifeStage(pet.birthDate, pet.species), })); });不需要手写太多业务代码,但每次让 Claude 生成完新接口,我会追加一句固定的审查指令:
请检查这个路由是否有参数校验和错误分支,如果用户传入的 petId 不是合法格式,不能直接查询数据库。这句话能让它补上很多不必要的防御逻辑。
4.2 疫苗到期提醒的真实设计
疫苗提醒是这个项目里最体现业务规则的模块。常见疫苗不是打一针就结束,比如狂犬疫苗首次接种后 21 到 30 天要打加强针,之后每年一针。为了让 AI 准确实现,我把规则描述得很具体:
每种疫苗模板包含 intervalDays 和时间次数 times。 首次接种后,系统生成下一条 scheduled 状态记录,日期为当前次接种日 + intervalDays。 用户手动标记“已完成接种”后,根据该模板再生成下一条记录。Claude Code 生成的调度函数如下:
export interface VaccineTemplate { name: string; times: number; intervalDays: number; } export interface VaccinationDose { doseNo: number; scheduledDate: Date; } export function buildVaccinationSchedule( template: VaccineTemplate, firstDoseDate: Date, ): VaccinationDose[] { const doses: VaccinationDose[] = []; let cursor = new Date(firstDoseDate); for (let i = 0; i < template.times; i++) { doses.push({ doseNo: i + 1, scheduledDate: new Date(cursor) }); cursor.setDate(cursor.getDate() + template.intervalDays); } return doses; }在这个基础上,我让 Claude Code 增加了一个“近 7 天到期提醒”的接口,用 SQL 的BETWEEN查询未来一周内待接种的记录。实际测试下来,这个模块的完成度很高,核心逻辑可以直接复用。
4.3 写单元测试的正确姿势
我试过让 Claude Code 直接“写测试”,它往往生成一些无效断言。更有效的做法是让它“先定义输入输出,再写实现”。我给它的指令是:
为 buildVaccinationSchedule 编写 Vitest 测试。 要求覆盖:单次疫苗、多次加强针、跨月跨年日期计算。 不要写 mock,直接构造真实 Date 对象。它生成的测试代码有一组非常关键的边界用例:
import { describe, it, expect } from 'vitest'; import { buildVaccinationSchedule } from './vaccineSchedule'; describe('buildVaccinationSchedule', () => { const template = { name: '狂犬疫苗', times: 3, intervalDays: 30, }; it('首针后按30天推进第二针', () => { const first = new Date('2025-01-01T00:00:00Z'); const doses = buildVaccinationSchedule(template, first); expect(doses[1].scheduledDate.toISOString()).toBe('2025-01-31T00:00:00.000Z'); }); it('跨年计算正常', () => { const first = new Date('2025-12-01T00:00:00Z'); const doses = buildVaccinationSchedule(template, first); expect(doses[2].scheduledDate.getUTCFullYear()).toBe(2026); }); });这种“测试先行”的提示词模式,能有效逼着 Claude Code 把隐含假设显式化。比如它会认真考虑跨年的天数累加,而不是简单写死一个月。
5. 前端界面:宠物卡片、疫苗时间线和体重图表
5.1 先用 Claude Code 定设计约束
前端部分我没有提供设计稿,只给了风格方向:
移动端优先,整体风格温暖但不幼稚。 卡片圆角偏大,使用浅绿色和暖黄色作为主色调,不用深色主题。 所有宠物卡片都要展示生命阶段徽章。 组件文件放在 web/src/components 下,样式统一用 Tailwind 原子类。Claude Code 生成的第一版界面确实符合“温暖”的方向,但它把很多装饰性内容写进了组件里,比如宠物头像的默认插画、底部悬浮按钮等。这些可以保留,但关键是我要求所有数据通过 props 传入,不允许组件内部直接写死示例数据。
5.2 核心组件的生成过程
我让 Claude Code 逐步生成组件,每个组件都追加一个明确的数据契约。比如宠物卡片的生成指令:
创建 PetCard 组件。 props 类型为 PetSummary,字段包括 id、name、species、birthDate、photoUrl。 组件内部使用 getLifeStage 计算生命阶段,使用 formatAge 展示年龄。 不写示例数据,由父组件传入。它生成的PetCard.tsx基本满足要求:
import { getLifeStage } from '../../utils/lifeStage'; import { formatAge } from '../../utils/formatAge'; import type { PetSummary } from '../../types/pet'; export function PetCard({ pet }: { pet: PetSummary }) { const stage = getLifeStage(pet.birthDate, pet.species); return ( <div className="rounded-2xl border border-emerald-100 bg-white p-4 shadow-sm"> <img src={pet.photoUrl} alt={pet.name} className="h-24 w-24 rounded-xl object-cover" /> <div className="mt-3"> <h2 className="text-lg font-semibold">{pet.name}</h2> <p className="text-sm text-gray-500"> {pet.species === 'DOG' ? '狗狗' : '猫咪'} · {pet.breed ?? '未知品种'} </p> <div className="mt-2 flex items-center gap-2"> <span className="rounded-full bg-amber-100 px-3 py-1 text-xs"> {stage === 'PUPPY' ? '幼年' : stage === 'ADULT' ? '成年' : '老年'} </span> <span className="text-xs text-gray-400">{formatAge(pet.birthDate)}</span> </div> </div> </div> ); }这个生成结果我基本没改。能直接用的原因,是因为我在创建组件前先定义了PetSummary类型文件,并且让getLifeStage、formatAge这些工具函数先存在。AI 在类型齐全的环境里生成组件,几乎不会出现凭空造字段的问题。
5.3 前后端联调与状态管理
前端通过 Vite 开发服务器访问后端,需要配置代理:
// web/vite.config.ts export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, }, }, }, });我给 Claude Code 的指令是“添加开发代理,使 web 端请求/api/pets能转发到 server 的 3000 端口”。它不仅是改配置,还提醒我在前端请求时统一封装request工具,方便处理统一返回结构。
export async function request<T>(path: string): Promise<T> { const res = await fetch(`/api${path}`); const json = await res.json(); if (json.code !== 0) { throw new Error(json.message); } return json.data as T; }联调阶段的真实体验是:Claude Code 生成的代码可以直接跑通大部分流程,但偶尔会出现 fetch 路径漏掉/api前缀,或者后端返回的日期字符串没有解析成 Date 对象。这些小问题通常靠一遍真实交互就能发现。
6. 从本地到服务器:可持续运行的部署流程
6.1 可复现的本地开发脚本
整个项目在根目录用 pnpm workspace 管理,本地开发只需要:
pnpm install pnpm dev根目录的package.json脚本是 Claude Code 生成的:
{ "scripts": { "dev": "concurrently \"pnpm --filter server dev\" \"pnpm --filter web dev\"", "build": "pnpm --filter server build && pnpm --filter web build", "lint": "pnpm --filter server lint && pnpm --filter web lint", "test": "pnpm --filter server test" } }使用concurrently能同时起两个进程,对本地联调很顺手。这个脚本被 AI 生成得足够简单清楚,说明它在工程化基础任务上确实扎实。
6.2 Docker 部署方案
后端和前端在同一个仓库里,我用 Docker 多阶段构建把两部分打包进一个容器。Claude Code 生成的 Dockerfile 大致如下:
FROM node:20-alpine AS base WORKDIR /app RUN corepack enable FROM base AS deps COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN pnpm install --frozen-lockfile FROM deps AS build COPY . . RUN pnpm build FROM node:20-alpine WORKDIR /app COPY --from=build /app/server/dist ./server/dist COPY --from=build /app/server/package.json ./server/package.json COPY --from=build /app/web/dist ./web/dist ENV NODE_ENV=production CMD ["node", "server/dist/index.js"]我额外让它生成一个docker-compose.yml,把数据库单独作为服务跑:
services: db: image: postgres:16-alpine environment: POSTGRES_USER: petapp POSTGRES_PASSWORD: change_me POSTGRES_DB: pet_life volumes: - pgdata:/var/lib/postgresql/data app: build: . ports: - "8080:3000" environment: DATABASE_URL: postgresql://petapp:change_me@db:5432/pet_life depends_on: - db volumes: pgdata:这里必须强调:配置里的密码不能直接用默认值,Claude Code 生成的 compose 文件只是模板,我后来把密码全部换成了环境变量注入,并且让.env文件不进入版本库。
6.3 自动化校验与部署前检查
在部署之前,我认为所有 AI 生成的代码都需要经过两道关卡:类型检查和生产构建。连续跑下面三条命令是最低门槛:
pnpm lint pnpm test pnpm build如果要再加一道保险,可以让 Claude Code 生成一个 CI 工作流文件,放在 Git 托管平台的 Actions 目录里。它能自动生成基本的lint + test + build流程,我不需要手写。这个流程文件本身不需要很复杂,但能拦截掉“本地能跑、仓库里拉下来就报错”的常见问题。
6.4 部署后的数据备份脚本
我让 Claude Code 顺手生成了一个数据库备份脚本,用 cron 每天凌晨备份一次 PostgreSQL:
#!/usr/bin/env bash BACKUP_DIR="/backups" mkdir -p "$BACKUP_DIR" pg_dump "$DATABASE_URL" > "$BACKUP_DIR/pet-life-$(date +%F).sql"别小看这一步,宠物健康数据对用户来说是不可再生资产。AI 工具生成的脚本往往能覆盖主要逻辑,但不会替你考虑“备份保留多少天”这种运维策略。我手动加了一条,只保留最近 30 天的备份,避免磁盘被撑爆。
7. 我踩过的 6 个坑:给用 Claude Code 开工的人提个醒
7.1 上下文失控是最常见的问题
我在项目后期让同一个会话同时改过前端样式、后端接口、数据库模型,结果 Claude Code 开始混淆文件路径。它在修改PetCard.tsx的时候,把getLifeStage的引用路径写到了 server 目录下。
解决办法很简单:每完成一个独立任务,就输入/clear清空上下文,然后用新的会话继续。不要高估长会话的容量,把一个长项目塞进一个会话里,产出质量会迅速下降。
7.2 不要把“方案确认”和“开始执行”混在一起
Claude Code 会在一些复杂需求前自动给出方案列表,比如“我建议用 A 方案,因为性能更好,但需要新增一个表”。如果你这时候直接回复“可以”,它可能会直接动手改数据库。正确做法是先要求它“只总结改动清单,不要执行,等确认后再操作”。把Review和Execute分成两步,能避免很多意外变更。
7.3 AI 会一本正经地编造不存在的 API
这可能是所有 AI 编码工具中最隐蔽的坑。让 Claude Code 调用某个 npm 包时,它可能凭训练记忆生成了一个“看起来合理但实际不存在”的函数签名。比如我让它写一个文件上传逻辑时,它把multer的某些中间件选项写错,代码能通过类型检查但运行时会报错。
因此,本地联调和真实环境测试是绝对不能省的步骤。不要让 AI 生成完代码就直接合入主分支,至少要跑一遍核心用户路径。
7.4 生成额外文件的“惊喜”
Claude Code 很喜欢顺手生成一些示例页面、模拟数据文件、README模板。在初期这些文档有帮助,但到了后期就成了噪音。比如项目根目录多出的EXAMPLES.md,和真实业务完全无关,却会误导新接手的人。我现在的习惯是:每次让它生成完关键模块,追加一句“不要创建额外说明文档”。
7.5 数据库迁移操作要谨慎授权
开发阶段我用prisma db push同步测试库很方便,但生产环境必须用prisma migrate deploy。如果让 Claude Code 直接在命令行里跑db push,它可能覆盖生产数据。我的做法是在server的package.json里分别定义db:dev和db:migrate脚本,并且明确告诉 AI 哪些命令不能碰。
7.6 权限和安全永远要人工复查
Claude Code 生成的接口默认往往是“登录即可访问”,但它不会主动帮你做资源归属校验。比如获取宠物详情时,任何登录用户都可以传一个别人的petId拿到数据。在发布前,我专门让它审查了所有/pets/:id类型接口,并补上了“当前用户只能操作自己的宠物”的过滤条件。
这种安全边界需要人来定义,不能指望 AI 自动完成。我会在提示词里明确写“所有按 id 查询宠物的接口,必须校验该宠物属于当前登录用户”,生成后也会做一次代码 review。
如果要说这次全流程实操给我最大的感受,那就是:Claude Code 已经完全可以当一个高效的全栈协作者,但我始终需要为它划定边界。项目开始前的需求拆解越清楚,生成质量就越高;每完成一个模块及时清空上下文,项目后期就不会混乱;测试和安全检查永远保留给人来做。
你可以把这套流程直接复用在其他领域的小型项目上,不一定是宠物管理,哪怕是记账本、库存系统、学习打卡工具,核心套路是一样的:先让它做产品分析,再定数据模型,然后逐步生成后端和前端,最后补测试和部署。真正的关键,不是问 AI “能帮我写什么”,而是问自己“这个需求我是否已经定义得足够精确”。