1. 项目缘起与整体设计思路
1.1 为什么选宠物生命周期管理这个方向
宠物生命周期管理这个选题,乍看像是个小众赛道,但真正做过调研就会发现,它覆盖的需求链条极长。从宠物领养或购买那一刻起,疫苗提醒、驱虫周期、体重曲线、发情期记录、配种管理、孕期跟踪、幼崽档案、老年病监测、临终关怀,一直到离世后的纪念页面,每一个环节都是独立的功能模块,又彼此存在数据依赖。这种“长链路、多状态、强时序”的业务特征,恰好是检验一套AI辅助开发流程是否真正能落地的绝佳试金石。
我选择用Claude Code从零搭建这个项目,核心目的不是做一个能上架的商业App,而是想验证一件事:在完全没有现成代码骨架的前提下,仅靠自然语言描述和逐步交互,能不能把一套包含数据建模、状态机设计、前后端联调、测试用例生成的完整项目跑通。如果能,那这套方法论就可以复用到其他领域;如果不能,也能清楚知道卡点在哪里。
1.2 技术选型的底层逻辑
技术栈的选择上,我定了几个硬约束:第一,必须支持快速迭代,因为AI生成的代码需要频繁调整;第二,类型系统要足够强,否则AI在生成复杂状态流转时容易出错;第三,生态要成熟,遇到问题能快速找到参考方案。
最终选型如下:
| 层级 | 技术选择 | 选型理由 |
|---|---|---|
| 前端框架 | React + TypeScript | 组件化程度高,AI对React代码的生成质量明显优于其他框架 |
| 状态管理 | Zustand | 轻量,样板代码少,AI不容易在冗余逻辑上犯错 |
| 后端框架 | Fastify + TypeScript | 性能好,插件体系清晰,类型推导友好 |
| 数据库 | PostgreSQL + Prisma | 关系型建模适合生命周期这种强关联场景,Prisma的schema描述语言对AI极其友好 |
| 测试 | Vitest + Playwright | 单元测试和端到端测试覆盖,AI生成测试用例的准确率较高 |
| 部署 | Docker Compose | 本地一键拉起全栈环境,减少环境差异导致的调试成本 |
这里重点说一下为什么选Prisma。宠物生命周期管理涉及大量一对多、多对多的关系,比如一只宠物对应多条疫苗记录、多条体重记录、多个健康事件。如果用MongoDB这类文档数据库,AI在生成聚合查询时经常会把关联逻辑写错。而Prisma的schema文件本身就是一种结构化描述,AI在读取schema后能准确理解实体关系,生成的查询代码正确率提升非常明显。
1.3 项目模块划分与AI协作策略
整个项目我拆成了六个核心模块,每个模块单独与AI进行一轮完整对话:
- 宠物档案模块:基础信息、品种、生日、绝育状态、芯片编号
- 健康记录模块:疫苗、驱虫、体检、用药、过敏史
- 成长追踪模块:体重、身高、饮食量、活动量
- 繁殖管理模块:发情周期、配种记录、孕期跟踪、幼崽档案
- 提醒引擎模块:基于时间规则和事件规则的双引擎提醒
- 纪念与归档模块:离世记录、纪念页面、数据导出
每个模块的协作流程固定为四步:先让AI输出数据模型,我人工审核关系是否合理;再让AI生成API接口定义;然后生成前端页面和交互逻辑;最后生成测试用例并跑通。这个流程看起来机械,但实测下来能大幅减少返工,因为数据模型一旦错了,后面全错。
注意:不要一次性把六个模块全部丢给AI让它一口气生成。上下文窗口再大,AI在多模块交叉时也会出现实体引用混乱。分模块推进,每个模块结束后把关键接口固化下来,再进入下一个模块。
2. 数据建模与核心状态机设计
2.1 宠物生命周期实体的建模要点
宠物生命周期管理的核心实体是Pet,但它不是一个孤立的表。围绕Pet,我需要建立一套完整的关系网络。在Prisma schema中,我让AI先根据我的自然语言描述生成初版,然后我逐条审核并调整。
初版AI生成的核心结构大致如下(经过我人工修正后的版本):
model Pet { id String @id @default(cuid()) name String species Species breed String? birthDate DateTime? gender Gender neutered Boolean @default(false) chipNumber String? @unique status PetStatus @default(ACTIVE) ownerId String owner Owner @relation(fields: [ownerId], references: [id]) healthRecords HealthRecord[] growthLogs GrowthLog[] breedingLogs BreedingLog[] reminders Reminder[] memorial Memorial? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } enum Species { DOG CAT BIRD RODENT REPTILE OTHER } enum Gender { MALE FEMALE UNKNOWN } enum PetStatus { ACTIVE PREGNANT NURSING CHRONIC_ILLNESS DECEASED ARCHIVED }这里有几个关键决策需要解释。第一,status字段用枚举而不是布尔值,因为宠物状态不是简单的“活着/死了”,而是存在孕期、哺乳期、慢性病管理等中间态。这些状态直接影响提醒引擎的行为,比如孕期宠物的疫苗提醒需要自动推迟。第二,chipNumber设为唯一索引,因为芯片编号是宠物的法定身份标识,重复会导致数据混乱。第三,memorial设计为可选的一对一关系,只有宠物离世后才会创建,避免在活跃宠物上产生冗余数据。
2.2 健康记录的多态设计
健康记录模块是最容易设计过度的地方。疫苗、驱虫、体检、用药,这四类记录有大量共同字段(日期、备注、执行机构),但也有各自特有字段(疫苗有批号、驱虫有药品类型、体检有各项指标、用药有剂量和频次)。
我最初让AI生成的是四张独立的表,但很快发现查询“某只宠物最近三个月的所有健康事件”时需要四次JOIN,而且前端展示时间线时要合并四个数据源,逻辑很啰嗦。后来改成单表加类型字段加JSON扩展字段的方案:
model HealthRecord { id String @id @default(cuid()) petId String pet Pet @relation(fields: [petId], references: [id]) type HealthRecordType recordDate DateTime provider String? notes String? metadata Json? attachments String[] createdAt DateTime @default(now()) } enum HealthRecordType { VACCINE DEWORMING CHECKUP MEDICATION SURGERY ALLERGY }metadata字段用JSON存储类型特有的数据。比如VACCINE类型存{ "vaccineName": "狂犬疫苗", "batchNumber": "2024A001", "nextDueDate": "2025-06-01" },CHECKUP类型存{ "weight": 5.2, "temperature": 38.5, "heartRate": 120 }。这样时间线查询只需要一次查询,前端根据type字段决定渲染哪种卡片。
实操心得:JSON字段虽然灵活,但一定要在应用层做schema校验。我用Zod为每种type定义了metadata的结构,AI生成API时自动带上校验逻辑,避免脏数据写入。
2.3 提醒引擎的状态机设计
提醒引擎是整个项目最复杂的部分。它不是简单的定时任务,而是需要根据宠物当前状态、历史记录、用户偏好动态计算下一次提醒时间。
我设计了一个双引擎架构:时间规则引擎和事件规则引擎。时间规则引擎处理固定周期提醒,比如“每年一次狂犬疫苗”;事件规则引擎处理依赖其他事件的提醒,比如“配种成功后第21天提醒做孕检”。
提醒的状态流转如下:
PENDING -> TRIGGERED -> ACKNOWLEDGED -> COMPLETED \-> SNOOZED -> TRIGGERED \-> DISMISSED这个状态机是AI帮我梳理的,我给它描述了业务场景后,它输出了状态图和转换条件。我审核时补充了一条:SNOOZED状态必须记录snoozeCount,超过三次自动转为DISMISSED并通知用户,避免用户无限推迟导致提醒失效。
model Reminder { id String @id @default(cuid()) petId String pet Pet @relation(fields: [petId], references: [id]) type ReminderType title String description String? dueDate DateTime status ReminderStatus @default(PENDING) snoozeCount Int @default(0) ruleConfig Json triggeredAt DateTime? completedAt DateTime? createdAt DateTime @default(now()) } enum ReminderStatus { PENDING TRIGGERED ACKNOWLEDGED SNOOZED COMPLETED DISMISSED }ruleConfig存储提醒的生成规则,比如{ "engine": "TIME", "interval": "YEARLY", "anchorDate": "2024-06-01" }或{ "engine": "EVENT", "triggerEvent": "BREEDING_SUCCESS", "offsetDays": 21 }。这样当宠物信息变更时,可以重新计算所有关联提醒的dueDate。
3. 核心功能模块的实操实现
3.1 宠物档案创建与校验逻辑
宠物档案创建看起来简单,但实际涉及大量校验。我让AI生成创建接口时,明确要求它处理以下边界情况:生日不能晚于今天、芯片编号格式校验(15位数字)、品种与物种的匹配校验(比如物种是猫,品种不能选“金毛”)。
AI生成的校验逻辑初版用了大量if-else,我让它重构成Zod schema:
const createPetSchema = z.object({ name: z.string().min(1).max(50), species: z.nativeEnum(Species), breed: z.string().optional(), birthDate: z.coerce.date().max(new Date(), "生日不能晚于今天").optional(), gender: z.nativeEnum(Gender), neutered: z.boolean().default(false), chipNumber: z.string().regex(/^\d{15}$/, "芯片编号必须为15位数字").optional(), ownerId: z.string().cuid(), }).refine((data) => { if (data.species === Species.CAT && data.breed) { return CAT_BREEDS.includes(data.breed); } if (data.species === Species.DOG && data.breed) { return DOG_BREEDS.includes(data.breed); } return true; }, { message: "品种与物种不匹配", path: ["breed"] });这里有个细节值得说:z.coerce.date()会自动把前端传来的ISO字符串转成Date对象,省去了手动转换。refine用于跨字段校验,品种列表我维护了一个常量文件,AI生成时自动引用了这个文件。
注意事项:芯片编号的校验规则各地不同,我这里用的是15位纯数字的通用格式。实际项目中应该把校验规则做成可配置的,因为不同地区的芯片标准确实存在差异。
3.2 健康记录时间线的查询优化
健康记录时间线是用户使用频率最高的页面。初始实现是简单的findMany加orderBy,但当记录超过500条时,页面加载明显变慢。我让AI分析查询计划后,做了三处优化。
第一,在petId和recordDate上建复合索引:
@@index([petId, recordDate(sort: Desc)])第二,分页改用游标分页而不是偏移分页。偏移分页在数据量大时性能下降明显,因为数据库需要扫描并跳过前面的行。游标分页基于recordDate和id做锚点:
const records = await prisma.healthRecord.findMany({ where: { petId }, orderBy: [{ recordDate: 'desc' }, { id: 'desc' }], take: 20, ...(cursor ? { skip: 1, cursor: { id: cursor } } : {}), });第三,前端用虚拟列表渲染。AI帮我生成了基于react-window的虚拟列表组件,只渲染可视区域内的记录卡片。实测下来,1000条记录的页面加载时间从3.2秒降到了0.4秒。
3.3 提醒引擎的定时任务实现
提醒引擎的后端实现我用了node-cron加数据库轮询的混合方案。纯cron的问题是精度不够,而且服务重启后内存中的任务会丢失。纯轮询的问题是频率太高会给数据库压力。
最终方案是:每小时执行一次扫描任务,查询未来24小时内所有PENDING状态的提醒,将它们加入一个内存队列,队列中的任务用setTimeout精确触发。服务重启时,扫描任务会自动重建队列。
async function scanUpcomingReminders() { const now = new Date(); const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); const reminders = await prisma.reminder.findMany({ where: { status: ReminderStatus.PENDING, dueDate: { gte: now, lte: tomorrow }, }, include: { pet: { include: { owner: true } } }, }); for (const reminder of reminders) { scheduleReminder(reminder); } } function scheduleReminder(reminder: Reminder & { pet: Pet & { owner: Owner } }) { const delay = reminder.dueDate.getTime() - Date.now(); if (delay <= 0) { triggerReminder(reminder.id); return; } const timer = setTimeout(() => { triggerReminder(reminder.id); }, delay); activeTimers.set(reminder.id, timer); }triggerReminder负责更新状态为TRIGGERED并发送推送通知。推送渠道我抽象了一个NotificationChannel接口,目前实现了站内信和邮件两种,后续可以扩展短信或即时通讯工具。
实操心得:
setTimeout的最大延迟值是24.8天左右,超过这个时间的定时器会立即触发。所以扫描窗口设为24小时是安全的。如果你需要更长的提前量,必须用分层扫描策略。
3.4 繁殖管理模块的状态流转
繁殖管理是业务逻辑最复杂的模块。一只母犬从发情到配种到生产,中间涉及多个状态转换,每个转换都有时间窗口和校验条件。
我让AI根据以下自然语言描述生成状态机:“母犬发情期持续约21天,最佳配种时间是发情第10到14天。配种后第21天可以做B超确认怀孕。怀孕期约63天,预产期前一周进入待产状态。生产后进入哺乳期,哺乳期约42天。”
AI生成的状态机包含六个状态:HEAT、MATING、PREGNANCY_CHECK、PREGNANT、LABOR、NURSING。每个状态有对应的enterCondition和exitCondition。我审核时发现它漏掉了“假孕”这个状态,实际中母犬配种后未受孕但表现出怀孕症状的情况很常见,需要单独处理。补充后状态变为七个。
const breedingStateMachine = { HEAT: { next: ['MATING', 'ENDED'], enterCondition: (pet) => pet.gender === 'FEMALE' && !pet.neutered, maxDurationDays: 21, }, MATING: { next: ['PREGNANCY_CHECK', 'ENDED'], enterCondition: (record) => record.matingDate !== null, maxDurationDays: 3, }, PREGNANCY_CHECK: { next: ['PREGNANT', 'FALSE_PREGNANCY'], enterCondition: (record) => daysSince(record.matingDate) >= 21, maxDurationDays: 7, }, // ... 其余状态 };每个状态转换都会触发提醒引擎重新计算相关提醒。比如进入PREGNANT状态后,自动生成“预产期提醒”“孕检提醒”“营养补充提醒”三条新提醒。
4. 常见问题与排查技巧实录
4.1 AI生成代码的典型错误模式
在跟Claude Code协作的过程中,我总结了它最容易犯的几类错误,以及对应的排查方法。
第一类:Prisma关系字段命名不一致。AI在生成schema时,有时会把关系字段命名为healthRecords,但在查询代码里写成healthRecord。这种错误TypeScript编译器会直接报错,排查成本低。我的做法是每生成一个模块的schema后,立即运行prisma generate,让类型系统先过一遍。
第二类:异步逻辑中的竞态条件。比如提醒触发时,AI生成的代码先查询提醒状态,再更新状态,中间没有加锁。如果两个定时任务同时触发同一条提醒,会导致重复通知。修复方法是用Prisma的updateMany加状态条件做原子更新:
const result = await prisma.reminder.updateMany({ where: { id: reminderId, status: ReminderStatus.PENDING }, data: { status: ReminderStatus.TRIGGERED, triggeredAt: new Date() }, }); if (result.count === 0) { // 说明已经被其他任务处理了,直接返回 return; }第三类:日期时区处理错误。AI生成的代码经常混用本地时间和UTC时间。比如new Date()返回的是本地时间,但数据库存的是UTC。修复方案是统一用UTC存储,前端展示时再转本地时区。我在项目里封装了一个toUTC和toLocal工具函数,所有日期操作必须经过这两个函数。
4.2 数据库迁移中的踩坑记录
Prisma的迁移在生产环境需要格外小心。我遇到过一次迁移导致数据丢失的情况:AI生成的迁移脚本在修改枚举类型时,直接删除了旧枚举并创建新枚举,导致所有引用旧枚举的记录被置空。
正确的做法是分步迁移:先添加新枚举值,再迁移数据,最后删除旧值。Prisma支持用--create-only参数生成迁移文件后手动编辑:
npx prisma migrate dev --create-only --name add_false_pregnancy_status然后手动编辑生成的SQL,确保数据安全。这个坑让我养成了一个习惯:任何涉及枚举变更或字段删除的迁移,必须先在生产数据的副本上跑一遍。
4.3 前端状态同步的常见问题
宠物档案修改后,关联的提醒列表需要同步更新。AI最初生成的代码是在修改成功后手动调用refetchReminders(),但这样容易遗漏,而且多个组件同时修改时会产生竞态。
后来改用Zustand的订阅机制,当pet store中的宠物信息变更时,自动触发reminder store的重新计算:
usePetStore.subscribe( (state) => state.currentPet, (pet) => { if (pet) { useReminderStore.getState().recalculateForPet(pet.id); } } );这样无论哪个组件修改了宠物信息,提醒列表都会自动同步,不需要手动调用刷新。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 提醒重复触发 | 竞态条件 | 检查triggerReminder是否原子更新 | 用updateMany加状态条件 |
| 时间线加载慢 | 缺少复合索引 | 用EXPLAIN分析查询计划 | 添加petId+recordDate索引 |
| 日期显示偏差 | 时区混用 | 检查数据库存储值和前端展示值 | 统一UTC存储,前端转本地 |
| 品种校验失效 | 品种列表未更新 | 检查常量文件是否包含新品种 | 将品种列表改为数据库配置 |
| 状态机卡死 | 缺少超时转换 | 检查每个状态的maxDurationDays | 添加定时任务强制转换超时状态 |
| 迁移丢数据 | 枚举直接替换 | 检查迁移SQL是否包含DROP | 分步迁移,先加后删 |
避坑技巧:每次AI生成完一个模块的代码后,不要急着写下一个模块。先跑一遍完整的测试用例,确保当前模块的数据流是通的。我吃过亏,连续生成了三个模块后才发现第一个模块的数据模型有问题,返工成本翻了三倍。
5. 测试策略与部署上线
5.1 AI辅助生成测试用例的实践
测试用例的生成是我觉得AI帮助最大的环节之一。我只需要描述业务场景,AI就能生成覆盖正常流程和边界情况的测试代码。比如提醒引擎的测试,我描述了“宠物疫苗到期前7天应触发提醒,到期当天应再次提醒,逾期3天未处理应标记为逾期”,AI生成了以下测试:
describe('ReminderEngine', () => { it('应在疫苗到期前7天触发提醒', async () => { const pet = await createTestPet(); const vaccine = await createHealthRecord(pet.id, { type: 'VACCINE', recordDate: new Date('2024-01-01'), metadata: { nextDueDate: '2024-07-01' }, }); await advanceTimeTo('2024-06-24'); const reminders = await getActiveReminders(pet.id); expect(reminders).toContainEqual( expect.objectContaining({ type: 'VACCINE_DUE', status: 'TRIGGERED' }) ); }); it('逾期3天未处理应标记为逾期', async () => { // ... }); });这里用了advanceTimeTo这个测试工具,它通过mock系统时间来实现时间旅行。AI生成的初版直接用了jest.useFakeTimers(),但我们的定时任务用了setTimeout,fake timers会导致测试挂起。后来改用了@sinonjs/fake-timers,可以精确控制时间推进而不影响异步任务。
5.2 端到端测试的关键场景
Playwright的端到端测试我重点覆盖了三个场景:完整的新宠物建档流程、健康记录添加与时间线展示、提醒的触发与处理。这三个场景覆盖了80%的用户操作路径。
AI生成的Playwright测试初版选择器用了CSS类名,但我们的样式用了CSS Modules,类名是哈希化的,测试很不稳定。后来统一改用>await page.getByTestId('pet-name-input').fill('小白'); await page.getByTestId('species-select').selectOption('DOG'); await page.getByTestId('submit-pet').click(); await expect(page.getByTestId('pet-card')).toContainText('小白');
实操心得:让AI生成测试时,明确告诉它使用
>FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine AS runner WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY package*.json ./ EXPOSE 3000 CMD ["node", "dist/main.js"]镜像体积降到了280MB。数据库的持久化用volume挂载,备份策略是每天凌晨用
pg_dump导出到宿主机目录,保留最近30天的备份。5.4 上线后的监控与告警
上线后我加了一个简单的监控面板,追踪几个关键指标:提醒触发成功率、API平均响应时间、数据库连接池使用率。提醒触发成功率低于99%时会触发告警,因为漏提醒对宠物健康管理来说是严重问题。
监控数据用Prometheus采集,Grafana展示。AI帮我生成了Prometheus的配置和Grafana的dashboard JSON,我只需要调整阈值和告警规则。这套监控跑下来,发现过一次数据库连接池泄漏的问题,原因是某个查询没有正确释放连接,修复后连接池使用率从95%降到了40%。
6. 个人实操体会与后续扩展方向
这套流程跑下来,我最大的体会是:AI辅助开发的核心不在于AI能写多少代码,而在于你能否把业务逻辑拆解成AI能准确理解的粒度。我试过一次性给AI描述整个提醒引擎的需求,它生成的代码结构混乱、边界处理缺失。但当我把它拆成“状态定义”“转换条件”“触发逻辑”“通知渠道”四个独立对话后,每个部分的代码质量都明显提升。
另一个体会是关于数据模型的重要性。在宠物生命周期管理这个场景里,数据模型一旦确定,后面的API、前端、测试都是水到渠成的事。我建议任何用AI从零搭建项目的朋友,把至少40%的时间花在数据建模和审核上,这个投入的回报率是最高的。
后续这个项目还可以往几个方向扩展。一是加入多宠物家庭的支持,目前的数据模型是Owner直接关联Pet,可以加一个Household层来支持多人共同管理宠物。二是接入宠物智能硬件的数据,比如智能项圈的活动量数据、智能猫砂盆的如厕记录,这些数据可以自动填充成长追踪模块。三是做一个开放API,让第三方宠物服务商(医院、寄养机构)可以授权访问部分数据,减少用户重复录入的麻烦。
最后分享一个小技巧:跟AI协作时,养成“先让它解释,再让它实现”的习惯。比如生成提醒引擎之前,我先让AI用自然语言描述它理解的提醒触发流程,确认理解一致后再让它写代码。这个习惯帮我避免了很多“代码写得很漂亮但逻辑完全不对”的情况。