☰
基于Spring Boot与Vue的宠物管理系统实战:从表设计到联调避坑
2026/10/4 5:04:48 网站建设 项目流程

简介:这是一套面向高校计算机专业学生与Java开发初学者的宠物管理系统完整项目源码,采用SpringBoot后端与Vue前端分离架构,可作为毕业设计、课程作业或全栈练手参考。系统功能覆盖用户注册登录与角色权限管理、宠物信息增删改查与照片上传、疫苗接种与健康检查提醒、美容洗澡体检等服务预约及评价、论坛发帖与好友关注私信,以及操作日志记录和数据备份恢复等系统管理模块,业务闭环较为完整。压缩包共471个文件,约10.81MB,其中154个Java文件承载后端业务逻辑,111个Vue文件与41个JavaScript、19个CSS文件构成前端页面与交互,另有25个XML配置、1个SQL建表脚本及大量png、jpg图片资源,前后端代码与数据库脚本齐备。目前已有132人学习下载,适合需要快速理解SpringBoot与Vue整合开发、对照模块划分梳理项目结构、或在此基础上二次开发与撰写论文的读者参考。

1. 宠物医院前台那台老电脑,为什么让我决定重写一套 Java 宠物管理系统

很多做 Java 课程设计或接私活的朋友,第一次听到「宠物管理系统」这五个字,脑子里蹦出来的多半是增删改查加一个登录页。我一开始也这么想,直到帮一家社区宠物诊所修他们那台用了六年的前台电脑。那台机器上跑的系统,宠物档案、疫苗记录、会员卡余额全塞在一张 Excel 里,医生想查一只猫的过敏史,得先按品种筛选再翻三页。更离谱的是,同一只狗因为主人换了手机号,在系统里存了两条记录,疫苗打重了一针。那天下午我就决定,用 Java 重新写一套能落地的宠物管理系统,不是交作业那种,是能让前台小姐姐十分钟上手、医生扫一眼就看清病史的那种。这套东西适合谁?适合正在找 Java 综合项目练手的在校生,也适合想用 Spring Boot 加 Vue 快速搭一套业务系统的初中级开发。它不炫技,但每一层都踩在真实需求上。

2. 需求拆解与技术选型:别急着写代码,先把这四张表想清楚

2.1 宠物管理系统的核心实体到底有几个

新手最容易犯的错,是打开 IDEA 就建 Controller。我一般会先拿一张 A4 纸,把系统里「谁跟谁发生关系」画出来。宠物管理系统看着简单,其实实体之间的耦合比商品管理系统还绕。核心实体我最后收敛到四个:宠物(Pet)、主人(Owner)、医生(Vet)、就诊记录(MedicalRecord)。宠物和主人是多对一,一只宠物只有一个主人,但一个主人可以养多只;就诊记录同时挂宠物和医生,一次就诊对应一只宠物和一位医生。疫苗记录我一开始想单独建表,后来发现它本质就是就诊记录的一种类型,加个 type 字段就够了,省一张表少一堆关联查询。

这里有个选型上的分水岭:宠物和主人的关系要不要做成多对多?我见过有人为了「支持宠物转让」把中间表都建好了。但真实场景里,转让宠物是低频操作,直接改 pet 表的 owner_id 就行,没必要上中间表。表结构一旦复杂,后面写 SQL 和调接口都是血泪。所以我的建议是,先把 80% 的高频场景覆盖住,剩下 20% 用状态字段或操作日志兜底。

2.2 为什么选 Spring Boot + MyBatis-Plus 而不是 JPA

技术选型这块,网上吵得最凶的就是 JPA 和 MyBatis 谁更好。放到宠物管理系统这个场景,我选 MyBatis-Plus,理由很实在。第一,宠物档案的查询条件特别碎,前台可能按主人手机号查,医生可能按品种加年龄查,用 JPA 的动态查询要么写 Specification 写到吐,要么上 QueryDSL 增加学习成本。MyBatis-Plus 的 LambdaQueryWrapper 几行就能拼出来,可读性对新手友好。第二,这个系统大概率要跑在诊所那种老机器上,MyBatis 的 SQL 可控,不会突然给你生成一条带十几个 join 的语句把内存打爆。

数据库我选 MySQL 8,字符集用 utf8mb4。别小看这个,宠物名字里带 emoji 的情况比你想象的多,utf8 三字节存不下,到时候插入报错你还得回头改表。连接池用 HikariCP,Spring Boot 默认就是它,不用额外配。前端我建议直接上 Vue3 + Element Plus,别用 JSP 或 Thymeleaf 了,前后端分离调试起来清爽,而且现在面试问「基于 Spring Boot + Vue 的商品管理系统」这类项目,前后端分离已经是默认预期。

2.3 从零建库:四张核心表的字段与索引设计

下面这段 SQL 是我实际用的建表语句,字段名和类型都经过几轮调整,可以直接抄。注意看索引部分,这是新手最容易漏的。

-- 主人表:手机号是高频查询入口,必须建唯一索引 CREATE TABLE owner ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '主人姓名', phone VARCHAR(20) NOT NULL COMMENT '手机号,登录和查询都用它', address VARCHAR(200) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_phone (phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物主人'; -- 宠物表:owner_id 外键关联,品种和名字常被搜索 CREATE TABLE pet ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '宠物名', species VARCHAR(20) NOT NULL COMMENT '物种:猫/狗/其他', breed VARCHAR(50) DEFAULT NULL COMMENT '品种', birth_date DATE DEFAULT NULL, weight DECIMAL(5,2) DEFAULT NULL COMMENT '体重kg', owner_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_owner (owner_id), KEY idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物档案'; -- 医生表:简单,但科室字段方便后续分诊 CREATE TABLE vet ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, department VARCHAR(50) DEFAULT NULL COMMENT '科室', title VARCHAR(50) DEFAULT NULL COMMENT '职称' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生'; -- 就诊记录:type 区分普通就诊和疫苗接种,避免多建表 CREATE TABLE medical_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, pet_id BIGINT NOT NULL, vet_id BIGINT NOT NULL, type TINYINT DEFAULT 1 COMMENT '1就诊 2疫苗', diagnosis VARCHAR(500) DEFAULT NULL, treatment VARCHAR(500) DEFAULT NULL, visit_time DATETIME NOT NULL, KEY idx_pet_time (pet_id, visit_time), KEY idx_vet (vet_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='就诊记录';

逻辑说明:owner 表的 phone 唯一索引不只是为了查得快,更是从数据库层面防止同一个主人被录两次,这是我在诊所看到重复档案后加的第一道防线。pet 表的 idx_owner 支撑「查某主人名下所有宠物」,idx_name 支撑前台模糊搜索。medical_record 的 idx_pet_time 是复合索引,因为查病史永远是「某只宠物按时间倒序」,单列索引在这里会回表。

参数说明:weight 用 DECIMAL(5,2) 而不是 FLOAT,因为体重这种数值不需要浮点精度,DECIMAL 存 0.01 精度足够且比较时不丢精度。type 用 TINYINT 而不是 ENUM,ENUM 改值要 ALTER TABLE,TINYINT 在代码里用常量映射更灵活。

2.4 接口分层:Controller 里别写业务逻辑

项目结构我按 controller、service、mapper、entity、dto 五层分。Controller 只做参数校验和调用 service,返回统一 Result 包装。Service 里写业务,比如「新增就诊记录时自动更新宠物体重」。Mapper 只做数据访问。DTO 用来接收前端参数,别直接用 Entity 接,否则前端传个 id 就能改别人数据。这套分层不是教条,是等你哪天要加权限校验或者换前端时,能少改一半代码。

3. 用 Spring Boot 把宠物档案和就诊记录跑通:从接口到联调

3.1 项目骨架与依赖:pom.xml 里必须有的几个坐标

新建 Spring Boot 项目,JDK 选 17,Spring Boot 版本用 3.2.x。依赖不用多,下面这几个是核心。

<dependencies> <!-- Web 接口 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus,注意用 boot3 专用 starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 参数校验,@NotBlank 这些注解靠它 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- Lombok,省 getter/setter --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

逻辑说明:MyBatis-Plus 在 Spring Boot 3 里必须用mybatis-plus-spring-boot3-starter,用老的mybatis-plus-boot-starter会报NoClassDefFoundError,这是升级 Boot 3 后最常见的翻车点。validation 依赖不加的话,@NotBlank注解不会生效,参数校验形同虚设。

参数说明:MyBatis-Plus 版本 3.5.5 是我验证过和 Boot 3.2 兼容的版本,再新的版本注意看官方兼容表。MySQL 驱动从 8.x 开始 groupId 变成com.mysql,老教程里的mysql-connector-java已经废弃。

3.2 宠物档案的增删改查:一个 Controller 讲透参数校验

先写 Pet 实体和 Mapper,MyBatis-Plus 让 Mapper 继承 BaseMapper 就有基础 CRUD。

@Data @TableName("pet") public class Pet { @TableId(type = IdType.AUTO) private Long id; @NotBlank(message = "宠物名不能为空") private String name; @NotBlank(message = "物种不能为空") private String species; private String breed; private LocalDate birthDate; private BigDecimal weight; @NotNull(message = "必须指定主人") private Long ownerId; private LocalDateTime createTime; } public interface PetMapper extends BaseMapper<Pet> { }

Controller 里新增宠物时,除了校验,还要检查 ownerId 是否真实存在,否则会造出孤儿宠物。

@RestController @RequestMapping("/api/pet") @RequiredArgsConstructor public class PetController { private final PetService petService; @PostMapping public Result<Pet> add(@RequestBody @Valid Pet pet) { // 校验主人是否存在,避免脏数据 petService.checkOwnerExists(pet.getOwnerId()); petService.save(pet); return Result.ok(pet); } @GetMapping("/search") public Result<List<Pet>> search(@RequestParam(required = false) String name, @RequestParam(required = false) String species) { // 条件为空时不拼该条件,避免全表扫描 LambdaQueryWrapper<Pet> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), Pet::getName, name) .eq(StringUtils.hasText(species), Pet::getSpecies, species) .orderByDesc(Pet::getCreateTime); return Result.ok(petService.list(wrapper)); } }

逻辑说明:@Valid触发实体上的校验注解,校验失败会抛 MethodArgumentNotValidException,需要配一个全局异常处理器把它转成友好的 JSON,否则前端收到的是 400 加一堆堆栈。search 接口用StringUtils.hasText做条件判断,这是 MyBatis-Plus 条件构造器的标准用法,条件为 false 时该片段不拼进 SQL。

参数说明:@RequestParam(required = false)让 name 和 species 都可选,前端可以只传一个。orderByDesc 按创建时间倒序,保证新录入的宠物排前面,符合前台使用习惯。

3.3 就诊记录与宠物档案的联动:一次事务里做完两件事

医生录入就诊记录时,如果这次称了体重,应该同步更新宠物档案里的 weight。这两步必须在一个事务里,否则记录写了体重没更新,数据就对不上。

@Service @RequiredArgsConstructor public class MedicalRecordService { private final MedicalRecordMapper recordMapper; private final PetMapper petMapper; @Transactional(rollbackFor = Exception.class) public void addRecord(MedicalRecord record, BigDecimal newWeight) { recordMapper.insert(record); if (newWeight != null) { Pet pet = new Pet(); pet.setId(record.getPetId()); pet.setWeight(newWeight); // updateById 只更新非 null 字段,不会覆盖其他属性 petMapper.updateById(pet); } } }

逻辑说明:@Transactional的rollbackFor = Exception.class很关键,默认只回滚 RuntimeException,如果中间抛了受检异常,事务不回滚,数据就脏了。updateById传一个只设了 id 和 weight 的对象,MyBatis-Plus 生成的 SQL 只会 set weight,不会把其他字段置空,这是它比全量 update 安全的地方。

参数说明:newWeight 允许为 null,因为不是每次就诊都称重。如果传了 null 就跳过更新,避免把已有体重覆盖成空。

3.4 前后端联调:跨域和统一返回格式这两个坑先填了

前端 Vue 跑在 5173 端口,后端 8080,浏览器直接拦跨域。开发阶段我一般在后端配一个全局 CORS,别在前端配代理绕,代理在生产环境还得再配一遍。

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }

逻辑说明:allowedOriginPatterns("*")配合allowCredentials(true)是 Spring 5.3 之后的写法,老教程里的allowedOrigins("*")和 credentials 同时用会报错。maxAge 设 3600 秒,减少预检请求次数。

参数说明:生产环境要把allowedOriginPatterns改成具体域名,别留星号。统一返回格式我用一个 Result 类,code、msg、data 三个字段,前端拦截器统一判断 code 是否为 200,非 200 弹提示。这套约定定下来,后面加接口就是复制粘贴。

4. 避坑与排查:宠物管理系统开发中最容易翻车的五个地方

4.1 中文乱码:数据库、连接串、响应三处都要设 utf8mb4

现象:宠物名字存进去变成问号,或者前端显示乱码。原因:MySQL 建库时没指定字符集,或者 JDBC 连接串没加 characterEncoding。解决:建库语句末尾加DEFAULT CHARSET=utf8mb4,application.yml 里连接串加?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。三处缺一处都可能乱,我一般建完库先插一条带中文和 emoji 的数据验证。

4.2 时间字段差 8 小时:时区配置别只改一处

现象:就诊时间存进去是下午 3 点,查出来变成早上 7 点。原因:JDBC 连接串的 serverTimezone 和 JVM 默认时区不一致。解决:连接串统一写serverTimezone=Asia/Shanghai,同时启动类上加@PostConstruct设置TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"))。两个都设才稳,只设一个在某些云服务器上还是会偏。

4.3 删除主人时宠物变孤儿:外键约束和逻辑删除的取舍

现象:删了主人,宠物列表里那些宠物还在,但点进去查主人报空指针。原因:没做级联处理。解决:我一般不用数据库外键,而是在 Service 删除主人前先查名下有没有宠物,有就抛业务异常提示「请先转移或删除宠物」。如果业务允许级联,就在 pet 表加逻辑删除字段 deleted,删主人时批量标记宠物删除,查询时自动过滤。物理删除在业务系统里尽量少用,后悔药没得吃。

4.4 模糊查询导致全表扫描:like 的写法有讲究

现象:宠物数据到几千条后,按名字搜索越来越慢。原因:like '%关键词%'前置百分号让索引失效。解决:如果业务允许前缀匹配,改成like '关键词%',能走索引。如果必须中间匹配,数据量再大就上 Elasticsearch 或至少加个全文索引。课程设计阶段数据少感觉不出来,但这是面试官爱问的点,提前知道不吃亏。

4.5 并发录入同一主人:唯一索引报错要转成友好提示

现象:两个前台同时录入同一个手机号的主人,第二个报数据库唯一约束异常,页面弹一堆英文。原因:没捕获 DuplicateKeyException。解决:在全局异常处理器里单独捕获DuplicateKeyException,返回「该手机号已存在」的中文提示。别指望前端校验能拦住并发,最终防线永远在数据库唯一索引。

5. 让这套系统从能跑到好用:三个我压箱底的优化习惯

第一个习惯,给所有列表接口加分页。课程设计里数据就几条,你不分页也没事,但真实诊所一天几十条记录,一个月就上千。MyBatis-Plus 的 Page 对象配合 PaginationInnerInterceptor 插件,几行代码就能加上,前端 Element Plus 的 el-pagination 直接对接。分页参数我一般设默认每页 10 条,最大不超过 100,防止有人传 size=99999 把内存打爆。

第二个习惯,把高频查询做成缓存。宠物品种列表、医生列表这种几乎不变的数据,用 Spring Cache 加个@Cacheable注解,底层用 Caffeine 本地缓存,能省掉大量重复查询。注意缓存要设过期时间,别让医生信息改了半小时还刷不出来。我一般设 10 分钟过期,改数据时手动清一下缓存。

第三个习惯,接口返回的字段要克制。宠物详情接口别把主人所有信息都塞进去,前端要什么给什么。我见过有人把主人身份证号都返回给前端,这是安全隐患。用 DTO 做字段裁剪,敏感字段在序列化时用@JsonIgnore排除。这套系统虽然小,但养成这个习惯,以后做任何业务系统都不会出大问题。

最后说个验证方法:写完一个模块,别只在 Postman 里点两下。我会用 JMeter 或者简单的 for 循环脚本,往接口里灌 500 条宠物数据,然后看列表接口响应时间有没有明显上涨,看分页对不对,看模糊搜索还快不快。数据量是检验设计的最好尺子。这套宠物管理系统我从建表到联调大概花了三个周末,中间改过两次表结构,都是因为一开始没想清楚实体关系。希望你少走点弯路,先把表设计明白再动手写代码,后面会顺很多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询