1. 项目背景与需求分析
宠物医疗行业近年来呈现爆发式增长,传统的纸质记录和Excel管理方式已经无法满足现代宠物医院的需求。我在为本地一家中型宠物医院做技术咨询时,亲眼目睹了他们如何因为信息管理混乱而错失重要病例数据——前台接待员误删了一个月的就诊记录,导致无法追溯疫苗注射情况。这个事件促使我开发了这套基于Node.js的宠物医院就诊信息管理系统。
现代宠物医院需要处理的信息包括:
- 宠物基本信息(品种、年龄、病史等)
- 主人联系方式与消费记录
- 疫苗注射与病历档案
- 药品库存与处方管理
- 医生排班与就诊预约
2. 技术选型与架构设计
2.1 为什么选择Node.js?
在评估了PHP、Java和Python之后,我最终选择Node.js作为后端技术栈,主要基于三个实际考量:
I/O密集型场景优势:宠物医院系统90%的操作都是数据库CRUD,Node的非阻塞I/O模型比传统多线程架构更高效。在我的压力测试中,单台4核服务器用Express处理简单查询请求,QPS能达到1200+。
全JavaScript开发:从后端到前端统一使用JavaScript,减少了开发者的上下文切换成本。特别是使用MongoDB时,JSON数据格式可以无缝衔接前后端。
丰富的npm生态:比如我们使用的
multer处理病历图片上传、jsonwebtoken实现权限控制,这些成熟模块极大加快了开发进度。
2.2 系统架构图解
[前端] Vue.js + Element UI │ ├─ HTTP ── [API层] Express路由控制器 │ │ │ ├─ [业务逻辑] 服务层(就诊、药品、用户等模块) │ │ │ │ │ └─ [数据访问] Mongoose ODM │ │ │ │ └─ [中间件] 身份验证/日志/错误处理 │ │ └─ WebSocket ────────┤ │ [持久层] MongoDB集群(分片+副本集)这套架构在杭州某连锁宠物医院的实际部署中,平稳支撑了日均300+就诊记录的业务量。特别说明几个关键设计点:
- 采用MongoDB的分片集群,按宠物ID哈希分片,解决单个病历文档可能较大的问题(包含X光片等附件)
- WebSocket用于实时推送急诊通知,避免轮询造成的服务器压力
- 服务层按业务垂直拆分,便于后期微服务化改造
3. 核心功能实现细节
3.1 病历数据结构设计
宠物病历是系统的核心,我们采用嵌套文档结构:
// models/medicalRecord.js const recordSchema = new Schema({ pet: { type: Schema.Types.ObjectId, ref: 'Pet', required: true }, symptoms: { type: [String], // 症状标签化 index: true // 便于症状统计 }, diagnosis: { text: String, icd11Code: String // 国际疾病分类编码 }, treatments: [{ medicine: { type: Schema.Types.ObjectId, ref: 'Medicine' }, dosage: String, frequency: String }], images: [{ url: String, annotation: String, // 影像标注 uploadTime: { type: Date, default: Date.now } }], nextVisit: Date // 智能提醒功能依赖字段 });实战经验:最初我们使用MySQL关系型设计,但宠物病历的异构特性(不同病症需要不同字段)导致大量空字段。迁移到MongoDB后,存储空间节省了40%,查询性能提升3倍。
3.2 高并发预约处理
节假日是宠物医院的预约高峰,我们采用以下优化策略:
- Redis队列缓冲:使用LPUSH/RPOP实现预约请求排队
// services/appointment.js async function createAppointment(req) { const queueKey = `appt_queue:${req.clinicId}`; await redis.lpush(queueKey, JSON.stringify(req)); // 后台worker处理队列 }- 乐观锁控制库存:
const session = await Medicine.startSession(); session.startTransaction(); try { const med = await Medicine.findOneAndUpdate( { _id: id, stock: { $gte: quantity } }, { $inc: { stock: -quantity } }, { session } ); if (!med) throw new Error('库存不足'); await session.commitTransaction(); } catch (err) { await session.abortTransaction(); throw err; } finally { session.endSession(); }- 限流保护:使用
express-rate-limit限制同一IP的频繁请求
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 100 // 每个IP最多100次请求 }); app.use('/api/appointments', limiter);4. 典型问题排查实录
4.1 文件上传内存溢出
初期上线时,有医院反馈上传宠物CT影像时服务崩溃。通过以下步骤定位问题:
- 使用
v8-profiler抓取内存快照 - 发现
multer的默认内存存储导致大文件堆积 - 改造为直接流式传输到S3:
const upload = multer({ storage: multerS3({ s3: s3Client, bucket: 'pet-hospital', metadata: (req, file, cb) => { cb(null, { fieldName: file.fieldname }); }, key: (req, file, cb) => { cb(null, `records/${Date.now()}_${file.originalname}`); } }), limits: { fileSize: 50 * 1024 * 1024 } // 限制50MB });4.2 聚合查询性能优化
统计各品种宠物发病率时,发现MongoDB聚合查询超时。通过explain()分析发现:
- 缺少复合索引:在
symptoms和pet.breed上建立联合索引
recordSchema.index({ symptoms: 1, 'pet.breed': 1 });- 使用
$facet分阶段处理:
const pipeline = [ { $match: { clinicId } }, { $facet: { byBreed: [ { $group: { _id: "$pet.breed", count: { $sum: 1 } } } ], bySymptom: [ { $unwind: "$symptoms" }, { $group: { _id: "$symptoms", count: { $sum: 1 } } } ] }} ];优化后查询时间从12秒降至800毫秒。
5. 部署与监控方案
5.1 PM2集群模式部署
通过PM2充分利用多核CPU:
pm2 start server.js -i max --name "pet-hospital"配置ecosystem文件实现零停机更新:
module.exports = { apps: [{ name: "pet-hospital", script: "./server.js", instances: "max", exec_mode: "cluster", max_memory_restart: "1G", env: { NODE_ENV: "production", } }] }5.2 监控指标采集
使用prom-client暴露关键指标:
const client = require('prom-client'); const collectDefaultMetrics = client.collectDefaultMetrics; collectDefaultMetrics({ timeout: 5000 }); // 自定义业务指标 const appointmentCounter = new client.Counter({ name: 'appointments_total', help: 'Total appointment requests', labelNames: ['status'] }); // 在路由中计数 app.post('/appointments', (req, res) => { appointmentCounter.inc({ status: 'created' }); });配合Grafana仪表盘监控:
- API响应时间P99
- MongoDB操作延迟
- 内存使用率
- 在线问诊会话数
6. 项目扩展方向
在实际运营中,我们陆续收到了一些有价值的反馈,正在规划这些增强功能:
- AI辅助诊断:集成TensorFlow.js实现皮肤病图像识别
const model = await tf.loadGraphModel('skin-disease-model.json'); const tensor = tf.browser.fromPixels(image) .resizeNearestNeighbor([224, 224]) .toFloat(); const predictions = model.predict(tensor);微信小程序接入:使用
taro框架开发跨端应用,通过JWT实现安全认证区块链存证:将疫苗记录上链,使用Hyperledger Fabric实现防篡改
这个项目让我深刻体会到:好的技术方案必须源于真实的业务痛点。最初我们过度设计了很多"酷炫"功能,后来通过与一线兽医的持续沟通,才逐渐聚焦到他们真正需要的核心特性——快速录入、智能提醒和稳定可靠。