ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Spring Boot与Vue的宠物管理系统实战:从表设计到联调避坑

基于Spring Boot与Vue的宠物管理系统实战:从表设计到联调避坑 简介这是一套面向高校计算机专业学生与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 三字节存不下到时候插入报错你还得回头改表。连接池用 HikariCPSpring 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 职称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT就诊记录;逻辑说明owner 表的 phone 唯一索引不只是为了查得快更是从数据库层面防止同一个主人被录两次这是我在诊所看到重复档案后加的第一道防线。pet 表的 idx_owner 支撑「查某主人名下所有宠物」idx_name 支撑前台模糊搜索。medical_record 的 idx_pet_time 是复合索引因为查病史永远是「某只宠物按时间倒序」单列索引在这里会回表。参数说明weight 用 DECIMAL(5,2) 而不是 FLOAT因为体重这种数值不需要浮点精度DECIMAL 存 0.01 精度足够且比较时不丢精度。type 用 TINYINT 而不是 ENUMENUM 改值要 ALTER TABLETINYINT 在代码里用常量映射更灵活。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 选 17Spring Boot 版本用 3.2.x。依赖不用多下面这几个是核心。dependencies !-- Web 接口 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus注意用 boot3 专用 starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 参数校验NotBlank 这些注解靠它 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- Lombok省 getter/setter -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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 实体和 MapperMyBatis-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 BaseMapperPet { }Controller 里新增宠物时除了校验还要检查 ownerId 是否真实存在否则会造出孤儿宠物。RestController RequestMapping(/api/pet) RequiredArgsConstructor public class PetController { private final PetService petService; PostMapping public ResultPet add(RequestBody Valid Pet pet) { // 校验主人是否存在避免脏数据 petService.checkOwnerExists(pet.getOwnerId()); petService.save(pet); return Result.ok(pet); } GetMapping(/search) public ResultListPet search(RequestParam(required false) String name, RequestParam(required false) String species) { // 条件为空时不拼该条件避免全表扫描 LambdaQueryWrapperPet 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 CHARSETutf8mb4application.yml 里连接串加?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。三处缺一处都可能乱我一般建完库先插一条带中文和 emoji 的数据验证。4.2 时间字段差 8 小时时区配置别只改一处现象就诊时间存进去是下午 3 点查出来变成早上 7 点。原因JDBC 连接串的 serverTimezone 和 JVM 默认时区不一致。解决连接串统一写serverTimezoneAsia/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防止有人传 size99999 把内存打爆。第二个习惯把高频查询做成缓存。宠物品种列表、医生列表这种几乎不变的数据用 Spring Cache 加个Cacheable注解底层用 Caffeine 本地缓存能省掉大量重复查询。注意缓存要设过期时间别让医生信息改了半小时还刷不出来。我一般设 10 分钟过期改数据时手动清一下缓存。第三个习惯接口返回的字段要克制。宠物详情接口别把主人所有信息都塞进去前端要什么给什么。我见过有人把主人身份证号都返回给前端这是安全隐患。用 DTO 做字段裁剪敏感字段在序列化时用JsonIgnore排除。这套系统虽然小但养成这个习惯以后做任何业务系统都不会出大问题。最后说个验证方法写完一个模块别只在 Postman 里点两下。我会用 JMeter 或者简单的 for 循环脚本往接口里灌 500 条宠物数据然后看列表接口响应时间有没有明显上涨看分页对不对看模糊搜索还快不快。数据量是检验设计的最好尺子。这套宠物管理系统我从建表到联调大概花了三个周末中间改过两次表结构都是因为一开始没想清楚实体关系。希望你少走点弯路先把表设计明白再动手写代码后面会顺很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表