公司动态

Spring Boot + Vue 构建宠物用药管理系统:定时任务与库存管理实战

📅 2026/8/12 14:42:34
Spring Boot + Vue 构建宠物用药管理系统:定时任务与库存管理实战
最近在开发一个宠物健康管理应用时遇到了一个有趣的需求如何为“嗑药猫猫”这类需要长期、定时服药的宠物设计一套可靠、易用且可扩展的用药提醒与记录系统。这不仅仅是设置一个闹钟那么简单它涉及到药品库存管理、用药历史追溯、剂量计算以及异常情况如漏服、呕吐的处理逻辑。本文将分享一套基于 Spring Boot Vue 的前后端分离实战方案从需求分析、数据库设计到核心代码实现手把手带你构建一个完整的“宠物用药管理”模块。无论你是想学习全栈开发还是正在为类似业务场景寻找解决方案都能从中获得可直接复用的代码和设计思路。1. 背景与核心概念为什么需要专门的宠物用药管理在宠物饲养特别是患有慢性病如肾病、甲状腺功能亢进或术后恢复期的宠物家庭中用药管理是一个高频且严肃的日常任务。我们戏称的“嗑药猫猫”其背后是宠物主面临的真实痛点药品复杂可能同时服用多种药物每种药的频率每日一次、两次、剂量半片、1.5ml、用法餐前、餐后都不同。容易遗忘人工记忆和手动设置手机闹钟在忙碌的生活中极易漏掉且难以管理多只宠物或多个药品。记录缺失是否按时服药、服药后有无不良反应这些历史记录对于宠物医生调整治疗方案至关重要但纸质记录不便查询和统计。库存预警药品即将用完时需要及时提醒主人购买或续方避免断药风险。因此一个数字化的“宠物用药管理系统”核心价值在于通过系统化的提醒、记录与统计提升宠物用药的依从性与安全性并为健康管理提供数据支持。从技术角度看该系统属于典型的CRUD增删改查与定时任务结合的应用。我们将使用以下技术栈后端Spring Boot (Web, JPA), MySQL前端Vue 3 Element Plus定时提醒SpringScheduled注解或集成 Quartz 用于复杂调度消息推送模拟站内消息实际可扩展为邮件、短信或微信推送。2. 环境准备与版本说明在开始编码前请确保你的开发环境已就绪。以下是本文示例所使用的主要环境与版本你可以根据实际情况进行调整。后端环境JDK: 17 或以上推荐 17LTS 版本稳定构建工具: Maven 3.6 或 Gradle 7.xIDE: IntelliJ IDEA 或 Eclipse (STS)数据库: MySQL 8.0 (5.7也可但建议使用8.0以获得更好的JSON支持等特性)主要依赖:Spring Boot: 2.7.x 或 3.x (本文示例基于 2.7.18与 3.x 在包路径上略有不同请注意调整)Spring Data JPAMySQL Connector前端环境Node.js: 16.x 或以上包管理工具: npm 或 yarn主要依赖:Vue 3Element PlusAxiosDay.js (用于日期处理)项目结构预览pet-medication-backend/ ├── src/main/java/com/example/petmedication/ │ ├── controller/ # 控制器处理HTTP请求 │ ├── entity/ # JPA实体类对应数据库表 │ ├── repository/ # 数据访问层接口 │ ├── service/ # 业务逻辑层 │ │ └── impl/ │ ├── dto/ # 数据传输对象 │ ├── scheduler/ # 定时任务 │ └── PetMedicationApplication.java # 启动类 ├── src/main/resources/ │ ├── application.yml # 配置文件 │ └── ... └── pom.xml pet-medication-frontend/ ├── public/ ├── src/ │ ├── api/ # 封装axios请求 │ ├── views/ # 页面组件 │ ├── components/ # 可复用组件 │ ├── router/ # 路由配置 │ ├── store/ # 状态管理 (Pinia/Vuex) │ └── App.vue ├── package.json └── vite.config.js # 或 vue.config.js3. 核心数据模型与业务逻辑拆解系统的核心是数据模型。我们先设计数据库表这决定了业务逻辑的边界。3.1 数据库表设计我们主要设计四张核心表pet宠物、medicine药品、medication_plan用药计划、medication_record用药记录。-- 1. 宠物表 CREATE TABLE pet ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 宠物名称, type varchar(20) DEFAULT CAT COMMENT 宠物类型CAT, DOG等, birthday date DEFAULT NULL COMMENT 出生日期, avatar_url varchar(255) DEFAULT NULL COMMENT 头像URL, owner_id bigint DEFAULT NULL COMMENT 关联的用户ID简化版实际应有用户表, created_time datetime DEFAULT CURRENT_TIMESTAMP, updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物信息表; -- 2. 药品库表 CREATE TABLE medicine ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 药品通用名, brand varchar(100) DEFAULT NULL COMMENT 品牌/商品名, specification varchar(100) DEFAULT NULL COMMENT 规格如 50mg/片10ml/瓶, stock int DEFAULT 0 COMMENT 当前库存数量, stock_unit varchar(20) DEFAULT NULL COMMENT 库存单位如 片、瓶、支, low_stock_threshold int DEFAULT 5 COMMENT 低库存预警阈值, created_time datetime DEFAULT CURRENT_TIMESTAMP, updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT药品信息表; -- 3. 用药计划表核心 CREATE TABLE medication_plan ( id bigint NOT NULL AUTO_INCREMENT, pet_id bigint NOT NULL COMMENT 关联宠物ID, medicine_id bigint NOT NULL COMMENT 关联药品ID, dosage decimal(10,2) NOT NULL COMMENT 单次剂量如 0.5 (片), dosage_unit varchar(20) DEFAULT NULL COMMENT 剂量单位如 片、ml, frequency varchar(50) NOT NULL COMMENT 用药频率Cron表达式或描述如 \0 0 8,20 * * ?\ (每日8点和20点), start_date date NOT NULL COMMENT 计划开始日期, end_date date DEFAULT NULL COMMENT 计划结束日期NULL表示长期, status tinyint DEFAULT 1 COMMENT 状态1-生效中0-已暂停-1-已结束, remark varchar(255) DEFAULT NULL COMMENT 备注如餐前服用, next_trigger_time datetime DEFAULT NULL COMMENT 下一次触发提醒的时间用于优化查询, created_time datetime DEFAULT CURRENT_TIMESTAMP, updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_pet_id (pet_id), KEY idx_next_trigger_time (next_trigger_time), CONSTRAINT fk_plan_pet FOREIGN KEY (pet_id) REFERENCES pet (id), CONSTRAINT fk_plan_medicine FOREIGN KEY (medicine_id) REFERENCES medicine (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用药计划表; -- 4. 用药记录表 CREATE TABLE medication_record ( id bigint NOT NULL AUTO_INCREMENT, plan_id bigint NOT NULL COMMENT 关联的用药计划ID, scheduled_time datetime NOT NULL COMMENT 计划服药时间, actual_time datetime DEFAULT NULL COMMENT 实际服药时间, status varchar(20) DEFAULT PENDING COMMENT 状态PENDING-待执行TAKEN-已服用SKIPPED-跳过MISSED-漏服, notes varchar(500) DEFAULT NULL COMMENT 记录备注如“呕吐了半片”, created_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_plan_id_status (plan_id, status), KEY idx_scheduled_time (scheduled_time), CONSTRAINT fk_record_plan FOREIGN KEY (plan_id) REFERENCES medication_plan (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用药记录表;设计要点解析medication_plan.frequency: 使用Cron表达式存储用药频率这是实现灵活定时的关键。例如0 0 8,20 * * ?表示每天上午8点和晚上8点。对于非技术人员前端可以提供可视化选择器如“每日两次”再转换为Cron表达式。medication_plan.next_trigger_time: 这是一个重要的优化字段。定时任务不需要每次都解析所有计划的Cron表达式来计算下次时间。我们可以在计划创建/更新时或每次触发后计算并更新这个时间。定时任务只需查询next_trigger_time NOW()的计划即可大大提升性能。medication_record: 记录每次计划执行的结果。actual_time为空表示未服用结合status可以清晰追踪每次用药情况。3.2 核心业务逻辑流计划创建用户为宠物选择药品、设置剂量、频率Cron后系统创建计划并计算首次next_trigger_time存入数据库。定时扫描一个后台定时任务例如每分钟执行一次查询所有next_trigger_time 当前时间且状态为“生效中”的计划。生成待办记录对于每个到点的计划在medication_record表中插入一条状态为PENDING的记录。这是核心动作它将抽象的“计划”转化为具体的、可操作的“待办事项”。更新下次触发时间根据该计划的Cron表达式计算出下一次触发的时间更新回medication_plan.next_trigger_time。前端提醒前端通过轮询或WebSocket获取当前用户所有状态为PENDING的用药记录进行弹窗、声音或通知栏提醒。用户操作用户点击“已服用”、“跳过”或“漏服”前端调用接口更新对应medication_record的status和actual_time。库存扣减当用户确认“已服用”时后端服务应同步扣减medicine表中的stock并检查是否触发低库存预警。4. 完整实战案例后端核心代码实现我们聚焦于后端Spring Boot的实现这是业务逻辑的核心。4.1 项目初始化与依赖首先创建一个Spring Boot项目在pom.xml中添加必要依赖。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.example/groupId artifactIdpet-medication/artifactId version0.0.1-SNAPSHOT/version namepet-medication/name descriptionPet Medication Management System/description properties java.version17/java.version /properties dependencies !-- Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JPA -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency !-- MySQL -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Lombok (可选简化代码) -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin /plugins /build /project4.2 实体类Entity定义对应上述数据库表创建JPA实体类。这里以MedicationPlan和MedicationRecord为例。// 文件路径src/main/java/com/example/petmedication/entity/MedicationPlan.java package com.example.petmedication.entity; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import javax.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; Entity Table(name medication_plan) Data public class MedicationPlan { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ManyToOne JoinColumn(name pet_id, nullable false) private Pet pet; ManyToOne JoinColumn(name medicine_id, nullable false) private Medicine medicine; Column(nullable false, precision 10, scale 2) private Double dosage; private String dosageUnit; Column(nullable false) private String frequency; // Cron表达式 Column(nullable false) private LocalDate startDate; private LocalDate endDate; private Integer status 1; // 1生效0暂停-1结束 private String remark; Column(name next_trigger_time) private LocalDateTime nextTriggerTime; // 下次触发时间 CreationTimestamp private LocalDateTime createdTime; UpdateTimestamp private LocalDateTime updatedTime; // 计算下次触发时间的业务方法需要Cron解析器如org.springframework.scheduling.support.CronExpression public void calculateAndSetNextTriggerTime() { if (this.status ! 1) { this.nextTriggerTime null; return; } // 这里简化实际应使用 CronExpression 解析 this.frequency // 并从 this.startDate 或上一个触发时间开始计算下一个时间点 // 示例伪代码 // CronExpression cronExpr CronExpression.parse(this.frequency); // LocalDateTime base this.nextTriggerTime ! null ? this.nextTriggerTime : this.startDate.atStartOfDay(); // this.nextTriggerTime cronExpr.next(base); } }// 文件路径src/main/java/com/example/petmedication/entity/MedicationRecord.java package com.example.petmedication.entity; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import javax.persistence.*; import java.time.LocalDateTime; Entity Table(name medication_record) Data public class MedicationRecord { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ManyToOne JoinColumn(name plan_id, nullable false) private MedicationPlan plan; Column(nullable false) private LocalDateTime scheduledTime; // 计划服药时间 private LocalDateTime actualTime; // 实际服药时间 Column(length 20) private String status PENDING; // PENDING, TAKEN, SKIPPED, MISSED Column(length 500) private String notes; CreationTimestamp private LocalDateTime createdTime; }4.3 定时任务扫描并生成用药记录这是系统的“发动机”。我们创建一个服务类使用Spring的Scheduled注解来定时执行。// 文件路径src/main/java/com/example/petmedication/scheduler/MedicationReminderScheduler.java package com.example.petmedication.scheduler; import com.example.petmedication.entity.MedicationPlan; import com.example.petmedication.entity.MedicationRecord; import com.example.petmedication.repository.MedicationPlanRepository; import com.example.petmedication.repository.MedicationRecordRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; Component Slf4j public class MedicationReminderScheduler { Autowired private MedicationPlanRepository planRepository; Autowired private MedicationRecordRepository recordRepository; /** * 每分钟执行一次扫描需要提醒的用药计划 */ Scheduled(cron 0 * * * * ?) // 每分钟的0秒执行 Transactional public void scanAndGenerateRecords() { LocalDateTime now LocalDateTime.now(); log.info(开始扫描用药计划当前时间{}, now); // 1. 查询所有 next_trigger_time now 且状态为生效的计划 ListMedicationPlan duePlans planRepository .findByNextTriggerTimeLessThanEqualAndStatus(now, 1); if (duePlans.isEmpty()) { log.info(没有到期的用药计划。); return; } for (MedicationPlan plan : duePlans) { try { // 2. 为每个计划创建一条待办记录 MedicationRecord record new MedicationRecord(); record.setPlan(plan); record.setScheduledTime(now); // 计划时间就是当前扫描时间 record.setStatus(PENDING); recordRepository.save(record); log.info(为计划[ID:{}]生成待办记录。, plan.getId()); // 3. 更新计划的下次触发时间 // 这里调用实体类中的业务方法需要注入Cron解析器此处简化 // plan.calculateAndSetNextTriggerTime(); // 简化处理假设频率是每天同一时间则下次时间为明天此时 plan.setNextTriggerTime(now.plusDays(1)); planRepository.save(plan); } catch (Exception e) { log.error(处理用药计划[ID:{}]时发生异常, plan.getId(), e); // 此处可根据业务决定是否继续处理其他计划 } } log.info(用药计划扫描与记录生成完成。); } }关键点说明Scheduled(cron “0 * * * * ?”): 表示每分钟的第0秒执行。对于用药提醒分钟级精度通常足够。如果需要秒级可以调整。Transactional: 确保“创建记录”和“更新下次触发时间”在一个事务中要么都成功要么都失败避免数据不一致。性能依赖next_trigger_time索引查询效率很高。如果计划数量巨大十万级以上可以考虑分页查询。4.4 业务服务层处理用药确认与库存扣减当用户在前端点击“已服用”时需要调用后端接口更新记录状态并扣减库存。// 文件路径src/main/java/com/example/petmedication/service/impl/MedicationRecordServiceImpl.java package com.example.petmedication.service.impl; import com.example.petmedication.entity.MedicationRecord; import com.example.petmedication.entity.Medicine; import com.example.petmedication.repository.MedicationRecordRepository; import com.example.petmedication.repository.MedicineRepository; import com.example.petmedication.service.MedicationRecordService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.Optional; Service Slf4j public class MedicationRecordServiceImpl implements MedicationRecordService { Autowired private MedicationRecordRepository recordRepository; Autowired private MedicineRepository medicineRepository; Override Transactional public boolean confirmMedication(Long recordId, String notes) { OptionalMedicationRecord recordOpt recordRepository.findById(recordId); if (!recordOpt.isPresent()) { log.warn(用药记录[ID:{}]不存在。, recordId); return false; } MedicationRecord record recordOpt.get(); if (!PENDING.equals(record.getStatus())) { log.warn(用药记录[ID:{}]状态为{}无法确认服用。, recordId, record.getStatus()); return false; } // 1. 更新记录状态为“已服用” record.setStatus(TAKEN); record.setActualTime(LocalDateTime.now()); record.setNotes(notes); recordRepository.save(record); // 2. 扣减药品库存 Medicine medicine record.getPlan().getMedicine(); Integer currentStock medicine.getStock(); if (currentStock null || currentStock 0) { log.error(药品[ID:{}]库存不足或为空无法扣减。, medicine.getId()); // 这里可以抛出业务异常或者记录告警。为了流程继续我们仅记录日志。 } else { medicine.setStock(currentStock - 1); // 假设每次剂量消耗1个库存单位 medicineRepository.save(medicine); log.info(药品[ID:{}]库存扣减1当前库存{}, medicine.getId(), medicine.getStock()); // 3. 检查低库存预警 if (medicine.getStock() medicine.getLowStockThreshold()) { log.warn(药品[ID:{}, 名称:{}]库存低于阈值{}请及时补充, medicine.getId(), medicine.getName(), medicine.getLowStockThreshold()); // 此处应触发预警逻辑发送站内消息、邮件、短信等 // alertService.sendLowStockAlert(medicine); } } return true; } Override Transactional public boolean markAsSkipped(Long recordId, String notes) { // 标记为“跳过”逻辑类似但不扣减库存 return updateRecordStatus(recordId, SKIPPED, notes); } Override Transactional public boolean markAsMissed(Long recordId, String notes) { // 标记为“漏服”逻辑类似但不扣减库存 return updateRecordStatus(recordId, MISSED, notes); } private boolean updateRecordStatus(Long recordId, String targetStatus, String notes) { OptionalMedicationRecord recordOpt recordRepository.findById(recordId); if (!recordOpt.isPresent() || !PENDING.equals(recordOpt.get().getStatus())) { return false; } MedicationRecord record recordOpt.get(); record.setStatus(targetStatus); record.setNotes(notes); recordRepository.save(record); log.info(用药记录[ID:{}]状态更新为{}, recordId, targetStatus); return true; } }4.5 控制器层Controller提供REST API最后创建控制器暴露API供前端调用。// 文件路径src/main/java/com/example/petmedication/controller/MedicationRecordController.java package com.example.petmedication.controller; import com.example.petmedication.service.MedicationRecordService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; RestController RequestMapping(/api/medication-records) public class MedicationRecordController { Autowired private MedicationRecordService recordService; PostMapping(/{recordId}/confirm) public ResponseEntityMapString, Object confirmMedication( PathVariable Long recordId, RequestBody(required false) MapString, String requestBody) { String notes requestBody ! null ? requestBody.get(notes) : null; boolean success recordService.confirmMedication(recordId, notes); MapString, Object response new HashMap(); if (success) { response.put(code, 200); response.put(message, 用药确认成功); return ResponseEntity.ok(response); } else { response.put(code, 400); response.put(message, 用药确认失败记录不存在或状态不正确); return ResponseEntity.badRequest().body(response); } } PostMapping(/{recordId}/skip) public ResponseEntityMapString, Object skipMedication( PathVariable Long recordId, RequestBody(required false) MapString, String requestBody) { String notes requestBody ! null ? requestBody.get(notes) : null; boolean success recordService.markAsSkipped(recordId, notes); // 返回逻辑类似 confirmMedication略... } GetMapping(/pending) public ResponseEntity? getPendingRecords(RequestParam Long petId) { // 查询指定宠物所有状态为PENDING的记录返回给前端用于提醒 // 实现略调用对应的Service方法即可 } }5. 常见问题与排查思路在开发和部署此类系统时你可能会遇到以下典型问题。问题现象可能原因排查步骤与解决方案定时任务不执行1. 未在主类或配置类上添加EnableScheduling。2. Cron表达式错误。3. 任务方法被异常中断且未捕获。1. 检查启动类是否添加SpringBootApplication和EnableScheduling。2. 使用在线Cron表达式验证工具检查表达式。3. 在任务方法内添加try-catch并打印详细日志确保一个计划的失败不影响其他计划。用药记录重复生成1. 定时任务执行时间过短在上次任务未完成时又触发。2.next_trigger_time更新逻辑有误导致下次时间仍为过去时间。1. 考虑给任务加锁如Scheduled的fixedDelay替代cron或使用分布式锁。2. 仔细调试calculateAndSetNextTriggerTime方法确保计算的下次时间是未来的。可以在生成记录后立即打印更新前后的next_trigger_time进行对比。库存扣减出现负数并发操作导致。多个提醒同时被确认查询库存时都大于0然后依次扣减导致超卖。1.数据库层面在扣减SQL中使用条件stock 0例如UPDATE medicine SET stock stock - 1 WHERE id ? AND stock 0并通过返回值判断是否成功。2.应用层面对药品ID加锁如synchronized或分布式锁确保扣减操作的原子性。前端收不到实时提醒1. 前端轮询间隔太长。2. 后端查询pending记录的接口逻辑错误或性能慢。3. 未使用WebSocket等长连接技术。1. 适当缩短前端轮询间隔如30秒但需权衡服务器压力。2. 优化查询为(plan_id, status)和scheduled_time建立复合索引。3. 对于体验要求高的场景集成WebSocket或SSEServer-Sent Events在记录生成后主动推送给前端。Cron表达式不灵活用户需要“每两天一次”、“每周一和周三”等复杂规则标准Cron支持但配置不友好。1. 前端提供强大的可视化规则选择器将用户选择转换为标准Cron表达式。2. 后端可以引入更强大的调度库如Quartz它支持更复杂的CronTrigger和Calendar排除特定日期。6. 最佳实践与工程建议将系统从“能用”提升到“好用、稳定”还需要考虑以下工程实践。配置外部化与开关将定时任务的Cron表达式放在application.yml中便于不同环境开发、测试、生产调整频率而无需修改代码。pet-medication: scheduler: reminder-cron: 0 * * * * ? # 生产环境可以改为 0 */5 * * * ? 每5分钟 enabled: true # 可以增加开关在维护时关闭定时任务在代码中通过Value(${pet-medication.scheduler.reminder-cron})注入。异常处理与监控定时任务中的每个计划处理都要包裹try-catch避免单个计划失败导致整个任务中断。记录关键操作的日志INFO级别并监控错误日志ERROR级别。可以使用ELK或类似工具收集分析。对于库存扣减失败、低库存预警等关键业务异常应接入告警系统如钉钉、企业微信机器人。数据一致性保障如前所述库存扣减必须考虑并发使用乐观锁版本号或悲观锁SELECT ... FOR UPDATE是更严谨的做法。涉及多个表更新的操作如确认用药务必使用Transactional保证事务。可扩展性设计提醒渠道扩展当前是站内消息。可以抽象出一个NotificationService接口然后提供EmailNotificationService、SmsNotificationService、WechatNotificationService等实现通过配置决定使用哪种或哪几种。规则引擎对于更复杂的用药规则如“肝功能指标高于X时剂量减半”可以考虑引入轻量级规则引擎如Drools或自研规则解析器。分布式部署如果应用需要部署多实例定时任务会重复执行。此时需要引入分布式调度框架如Elastic-Job或XXL-Job确保同一任务在集群中只由一个实例执行。前端用户体验优化离线支持考虑宠物主可能处于无网络环境如地下室。前端可采用PWA渐进式Web应用技术支持离线时将操作记录在本地IndexedDB网络恢复后同步到服务器。多端同步确保Web端和移动端如果开发了App的用药状态能实时同步这需要后端API设计良好的状态机制前端使用WebSocket或频繁轮询。安全与权限所有API必须进行身份认证如JWT。/api/medication-records/pending接口必须校验传入的petId是否属于当前登录用户防止越权访问。涉及药品库存、用药记录等敏感数据的修改操作应记录详细的操作日志谁、在什么时间、做了什么。测试策略单元测试重点测试MedicationRecordServiceImpl中的业务逻辑特别是库存扣减的并发场景。集成测试测试定时任务MedicationReminderScheduler从扫描到生成记录的完整流程可以使用SpringBootTest配合内存数据库H2。Cron表达式测试编写一个工具类或单元测试验证前端传来的各种描述性规则是否能正确转换为Cron表达式并计算出预期的下次触发时间。通过以上步骤我们不仅实现了一个基础的“嗑药猫猫”用药提醒系统更构建了一个具备良好扩展性、稳定性和可维护性的业务模块。你可以在此基础上继续添加宠物健康数据记录、药品图片识别、与宠物医院系统对接等更多功能打造一个完整的宠物健康管理平台。