多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java宠物管理系统开发实战:从表结构设计到Spring Boot部署

Java宠物管理系统开发实战:从表结构设计到Spring Boot部署 简介基于Java的宠物管理系统设计与实现的完整毕业设计文档以docx格式呈现适合计算机相关专业学生在课程设计、毕业设计或项目实训中参考。文档系统阐述了基于SSM框架的宠物管理系统从需求分析、总体设计到具体实现的全过程覆盖管理员端、前台首页及用户端三大模块具体涉及宠物分类、商品管理、领养登记、宠物寄存、挂失管理、论坛交互及订单处理等核心功能并配有数据库MySQL设计思路与Java实现说明可帮助读者理解完整项目结构及开发流程。资源共1个文档文件约6.83MB内容包含摘要、目录、功能模块介绍及系统设计细节便于直接查阅和借鉴。目前已有73人学习适合需要快速梳理同类管理系统开发思路、撰写设计文档或搭建SSM项目框架的开发者参考。1. 基于Java的宠物管理系统先拆需求再决定用什么技术栈宠物管理系统是Java课程设计和中小型外包项目里出现频率极高的一类业务系统前后端加起来通常在三到五张表、十来个接口的量级很多刚转Java开发的人第一次完整接触SSM或Spring Boot项目就是从它开始的。这里的“宠物管理系统”不是某个固定开源项目的名字而是一类系统的统称核心是宠物档案登记、寄养/领养流程、疫苗接种提醒以及围绕宠物产生的订单和健康记录。它的难点不在CRUD本身而在角色权限、业务状态流转、文件上传和定时任务这几块。适合读这篇内容的人有两类一类是正在做毕业设计或者课堂项目需要在两个星期内把系统跑起来并写出设计文档另一类是在接外包单子帮宠物店做内部管理后台需要知道哪些坑会导致验收翻车。我会按一个可落地的单体架构来讲从建表到部署每个环节给出参数和命令尽量让你不用来回搜资料。2. 设计宠物管理系统的数据模型从角色权限到业务表字段2.1 角色设计管理员、店员、普通用户各自能看到什么宠物管理系统最常见的角色划分是三种管理员、店员或美容师、普通用户宠物主人。角色不同能看到的菜单和数据范围就不同。管理员负责系统配置、员工账号分配、查看所有订单和宠物档案店员只负责日常接待比如登记新宠物、录入洗澡美容订单、更新疫苗记录普通用户则是微信端或Web端登录后查看自己的宠物档案、预约服务、接收疫苗提醒。这个划分会直接影响后面的数据表设计因为“操作人”和“宠物主人”是两个不同的维度必须拆开。在做权限控制时我不建议一上来就引入Spring Security或者Shiro。对于三五张表的单体系统用拦截器加角色注解就够了。你在项目里可以先建一个user表字段包括user_id、username、password、role取值admin/staff/user、phone、create_time然后在Spring MVC的拦截器里判断session中的role决定哪些URL允许访问。如果你以后要扩展成多商户或者需要更细的权限粒度再升级到Shiro也不迟。2.2 核心业务表宠物档案、健康记录和订单表的关系宠物档案表pet_info是系统的地基。字段至少要覆盖pet_id、pet_name、pet_type猫/狗/兔子等、breed、gender、birthday、weight、avatar_url、owner_id关联user表、chip_id芯片编号、status在店/寄养中/已领养、create_time、update_time。owner_id是外键指向user表的主键用于区分这只宠物归属哪个用户。芯片编号在宠物医院和宠物店场景里很常用但要注意它是可选的如果没有就给默认值“未知”前端直接不显示。健康记录表health_record围绕每次就诊或疫苗注射建立字段包括record_id、pet_id、record_typevaccine/checkup/treatment、record_date、vet_name、detail、next_due_date、create_time。这里有一个容易被忽略的点疫苗提醒功能依赖next_due_date字段如果后端定时任务扫描的是“等于今天”的记录那么用户在录入疫苗时就必须把next_due_date算出来并落库。我见过很多项目把下次接种日期写成前端传来的一个字符串类型不对或者时区不对定时任务就扫不到。订单表orders则关联宠物和具体服务项目字段包括order_id、order_no、pet_id、user_id、service_typebath/grooming/boarding/medical、price、start_time、end_time、statuspending/processing/done/cancelled、remark。订单号不要用数据库自增ID业务上要求唯一并且看不出数量常见做法是Java里用“yyyyMMddHHmmss 四位随机数”拼或者直接用UUID去掉中划线。并发不高的情况下这个生成方式足够但如果同一秒内下单量超过几百还是建议引入Redis生成每日递增序号。2.3 外键、索引和数据类型建表时这几个参数要提前定好建立合表关系时我一般不会在数据库里强制加外键约束而是保留逻辑外键。原因很简单后期做数据清洗、批量导入时物理外键会导致插入顺序必须严格分层且MySQL在删除父表记录时容易出现锁等待。逻辑外键配合应用层的校验足够保证数据一致性性能也更好。表结构设计如下CREATE TABLE pet_info ( pet_id INT AUTO_INCREMENT PRIMARY KEY, pet_name VARCHAR(50) NOT NULL, pet_type VARCHAR(20) NOT NULL DEFAULT cat, breed VARCHAR(50), gender TINYINT NOT NULL DEFAULT 1, -- 1公 2母 birthday DATE, weight DECIMAL(5,2), avatar_url VARCHAR(255), owner_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0在店 1寄养 2已领养 create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_owner (owner_id), INDEX idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型上要特别留意宠物体重用DECIMAL而不是FLOAT避免浮点误差导致前端显示59.9999birthday用DATE类型而不是VARCHAR否则后面做年龄统计、生日提醒时还要做类型转换。创建时间和更新时间给上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP这样Java实体类里不用手动维护这两个字段。工程师在面试里经常会问“Java是静态链接的还是动态的”放在这个场景是有意义的你写JDBC驱动时如果依赖的是编译期固化版本的驱动类换MySQL驱动版本后可能会出现类加载冲突所以驱动jar包尽量用运行时依赖。3. 搭建Spring Boot工程并打通MyBatis数据访问层3.1 用Spring Initializr生成最小可用工程选对依赖版本无论你是用IDEA的Spring Initializr还是直接去官网生成依赖选择上只勾Web、MyBatis、MySQL Driver和Lombok就够了。用Spring Boot 2.7.x时mybatis-spring-boot-starter选择2.3.x版本用Spring Boot 3.x则要考虑mybatis-starter对Jakarta命名空间的支持。很多刚开始接触宠物管理系统的初学者会卡在版本兼容上Spring Boot 3.2配了一个旧版的MyBatis starter启动直接报ClassNotFoundException。我的习惯是直接看mvn仓库里starter的版本说明不要盲目选最新。生成完工程后先用一个最简单的Controller验证启动链路正常再做业务代码。这一步能隔离“环境没配好”和“业务代码有bug”两类问题在排查启动失败时就少一半干扰。3.2 application.yml配置数据库连接、MyBatis映射文件路径、日志级别配置文件是宠物管理系统里最容易出错的文件之一。我一般这样写server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mvc: static-path-pattern: /static/** mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.pet.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.example.pet.mapper: debugurl里的characterEncodingUTF8一定不要漏否则MySQL里中文全部变成问号这在后面排查时极其隐蔽。allowPublicKeyRetrievaltrue是MySQL 8.0驱动连接时需要的参数很多项目从MySQL 5.7迁移之后连接不上就是少了这个配置。打开MyBatis的map-underscore-to-camel-case后数据库字段owner_id就能自动映射成实体类里的ownerId不需要写冗长的resultMap。3.3 写第一个CRUD宠物档案新增和列表查询的完整代码宠物档案的Mapper接口如下只定义四个方法覆盖最常用的新增、按ID查询、分页列表、更新状态Mapper public interface PetInfoMapper { int insertPet(PetInfo petInfo); PetInfo selectPetById(int petId); ListPetInfo selectPetPage(Param(offset) int offset, Param(limit) int limit); int updatePetStatus(Param(petId) int petId, Param(status) int status); }对应的XML映射文件放在resources/mapper/PetInfoMapper.xml中?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN https://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.pet.mapper.PetInfoMapper insert idinsertPet parameterTypePetInfo useGeneratedKeystrue keyPropertypetId INSERT INTO pet_info (pet_name, pet_type, breed, gender, birthday, weight, avatar_url, owner_id, status) VALUES (#{petName}, #{petType}, #{breed}, #{gender}, #{birthday}, #{weight}, #{avatarUrl}, #{ownerId}, #{status}) /insert select idselectPetById resultTypePetInfo SELECT pet_id, pet_name, pet_type, breed, gender, birthday, weight, avatar_url, owner_id, status, create_time FROM pet_info WHERE pet_id #{petId} /select select idselectPetPage resultTypePetInfo SELECT pet_id, pet_name, pet_type, breed, owner_id, status FROM pet_info ORDER BY create_time DESC LIMIT #{offset}, #{limit} /select update idupdatePetStatus UPDATE pet_info SET status #{status} WHERE pet_id #{petId} /update /mapperinsert语句里useGeneratedKeystrue配合keyPropertypetId可以让数据插入后自动回填主键逻辑上新增完马上拿到自增ID方便继续插入健康记录。分页查询用LIMIT #{offset}, #{limit}offset由Service层根据“页数减一乘以每页条数”计算。日志里打开StdOutImpl之后在控制台能直接看到每条SQL的实际参数值省去手动拼接排查的时间。Maven工程里记得让src/main/resources目录里的xml被正确打包如果target里找不到xml文件MyBatis启动就会报Invalid bound statement这是第二常见的坑。4. 实现用户登录、文件上传与定时提醒宠物管理系统的核心功能模块4.1 用户登录与Session管理从表单提交到拦截器放行宠物管理系统的登录逻辑并不复杂用户在登录页输入账号密码Controller层把密码用MD5加盐哈希后与数据库比对成功后把user对象和role写入HttpSession。这里有两个改进点需要注意。第一密码加密不要直接用原始MD5现在更常见的做法是BCrypt但如果你使用的JDK版本较旧需要注意bcrypt算法会有性能开销登录接口的QPS会受影响。第二Session存储方式在小型单体项目里直接用默认的Tomcat Session不要把Spring Session的依赖引进来增加成本但收益很小。登录之后要拦截哪些路径我在一个Java配置类里维护了一个白名单数组/login、/register、/static/、/error其余所有路径都要求登录。角色控制用另一个拦截器去判断比如/admin/**路径只允许role为admin的账号进入否则跳转到403页面。这里给大家一个建议把拦截器里的白名单常量和角色规则放到配置文件中管理而不是硬编码在Java类里因为实际交付时客户经常会要求“这个页面不用登录也能看”。Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /static/**, /error); registry.addInterceptor(adminInterceptor) .addPathPatterns(/admin/**); }4.2 宠物图片上传本地存储与访问映射上传宠物头像或健康证明图片是宠物管理系统里必有的功能。我在本地开发时把文件存到项目的uploads目录下线上则直接配置Nginx指向这个目录做静态文件访问。先看上传接口的实现PostMapping(/upload) public String uploadPetAvatar(RequestParam(file) MultipartFile file, RequestParam(petId) int petId) { if (file.isEmpty()) { return 上传文件不能为空; } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName System.currentTimeMillis() _ petId ext; File dest new File(uploadDir newFileName); file.transferTo(dest); petInfoService.updateAvatar(petId, /uploads/ newFileName); return 上传成功; }newFileName用时间戳加petId拼出来避免用户上传的同名文件互相覆盖。transferTo方法在文件过大时会抛IOExceptionmultipart配置项里max-file-size设成5MB就是在这里生效的。更新头像URL之后前端img标签直接通过/uploads/xxx.jpg访问。Java开发工程师面试里常被问到的“静态链接”概念放在这里理解就是file.transferTo(dest)操作的是临时文件到目标文件的重定位和链接库没有关系但从JVM进程角度看任何文件IO都需要操作系统层面的句柄与JVM堆外内存协作。路径访问映射上Spring Boot需要单独放行uploads目录否则会被安全规则拦截Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadDir); }uploadDir变量可以配置成绝对路径比如/data/pet/uploads/Windows下则注意反斜杠转义。这里最常见的问题是开发环境相对路径能访问打包成jar之后相对路径跑到jar包内部去了一重启文件就丢失。比较稳妥的做法是启动参数里指定上传目录java -jar pet-system.jar --upload.dir/data/pet/uploads/然后在代码里读取这个属性。4.3 疫苗接种提醒基于Spring定时任务的到期扫描宠物管理系统里比较有业务价值的模块是疫苗提醒。需求通常是系统每天扫描一次health_record表查询next_due_date在7天内的记录给宠物主人手机号发一条提醒短信或者在系统内部生成一条待办消息。用Spring的Scheduled注解能直接实现定时扫描不需要额外引入Quartz依赖。Component public class VaccineRemindTask { Autowired private HealthRecordMapper healthRecordMapper; Scheduled(cron 0 0 8 * * ?) public void scanDueVaccines() { Date weekLater new Date(System.currentTimeMillis() 7L * 24 * 3600 * 1000); ListHealthRecord dueList healthRecordMapper.selectByDueDate(weekLater); for (HealthRecord record : dueList) { // 发送短信或站内信 remindService.sendRemind(record); } } }配合定时任务在Spring Boot启动类上加EnableScheduling注解即可。cron表达式“0 0 8 * * ?”表示每天早上8点执行一次。这里有两个坑第一Scheduled默认是单线程串行执行如果sendRemind里调了第三方短信接口且响应很慢很容易把任务周期拖垮建议在配置类里设置一个线程池让定时任务并行执行第二服务器时区如果不是Asia/Shanghai比如部署在基于UTC的镜像里早上8点执行会变成北京时间下午4点需要确保JVM时区正确。Configuration public class ScheduledConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }4.4 订单状态流转从“进行中”到“已完成”的边界校验宠物洗澡美容订单的状态流转是另一个业务核心。常见错误是Controller里直接接受页面传来的status值用户抓包改成cancelled就能绕过支付。正确做法是Service层提供一个updateOrderStatus方法内部校验当前状态到目标状态的切换是否合法public boolean updateOrderStatus(int orderId, int targetStatus) { Order order orderMapper.selectById(orderId); if (order.getStatus() 0 targetStatus 1) { return orderMapper.updateStatus(orderId, targetStatus) 0; } if (order.getStatus() 1 (targetStatus 2 || targetStatus 3)) { return orderMapper.updateStatus(orderId, targetStatus) 0; } throw new IllegalStateException(非法状态流转: order.getStatus() - targetStatus); }状态流转校验可以和Java枚举配合使用把每个状态的合法后继写在枚举里。这样做的好处是代码可读性高后面增加新状态时不会迷路。如果项目用了Lombok枚举里多写几个字段也不会让代码变重。5. 宠物管理系统开发避坑指南启动失败、中文乱码、图片丢失等7个典型问题5.1 现象Spring Boot启动失败端口被占用且没有服务运行启动日志里出现Web server failed to start. Port 8080 was already in use.第一反应是杀掉占用进程。Windows上用netstat -ano | findstr 8080查到PID后taskkill /F /PID pidLinux上用lsof -i:8080找到进程再kill -9。不过我遇到更隐蔽的一次是项目里同时引入了spring-boot-starter-web和spring-boot-starter-webflux导致启动时出现两个Web容器冲突JVM没有直接报端口占用而是报循环引用类错误排查了很久。所以第3章才强调依赖列表尽量精简。如果同一接口的URL路径有重复比如Controller上类注解用了/pet方法上又写全路径/pet/listSpring MVC启动时会覆盖Bean的映射关系调试时总感觉改的接口不生效。建议类注解和方法注解分开写类只管一级路由方法只写二级路径。5.2 现象中文内容写入数据库变成问号这个坑出现频率极高。现象是Java代码的字符串没有乱码console打印正常但数据库里存的是“???”。原因基本是数据库表字符集不是utf8mb4。MySQL 5.7创建表时如果建表语句不带CHARSET参数会用库的默认字符集很多老库默认是latin1中文必然出问题。我的解决方法是建库时就指定字符集CREATE DATABASE pet_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;我的习惯还包含连接层的URL参数useUnicodetruecharacterEncodingutf8。MySQL 8.0以上utf8在MySQL语义里已经等于utf8mb3不支持emoji字符集所以建表统一用utf8mb4更省心。如果你用的是Spring Boot 2.7连接池默认会从URL里读取编码参数不需要额外设置HikariCP的connectionInitSql。5.3 现象上传图片成功后刷新就404文件还在但访问不了前半段现象是图片能上传到本地目录但浏览器访问http://localhost:8080/uploads/xxx.jpg返回404。原因通常有两种。第一种是静态资源映射没生效也就是4.2节提到的那段addResourceHandlers没有执行检查配置类是否被ComponentScan扫描到或者项目里是否有多段路径映射互相覆盖。第二种是打包后文件路径问题本地开发代码使用的是./uploads相对路径打包成jar后当前工作目录变成jar所在目录上传的文件落在jar外层一旦应用以systemd服务方式启动工作目录又变成系统目录路径全乱了。解决方法是彻底放弃相对路径启动时通过--upload.dir/data/pet/uploads/指定绝对路径并把上传目录单独做成一个config类读取。这样不管是本地IDE还是服务器都是同一套逻辑。如果使用Docker部署记得把宿主机的数据目录挂载进来否则容器一重建数据全部丢失这个坑往往在验收时最为致命。5.4 现象MyBatis查询结果字段全是NULL但数据库里明显有数据这个问题的核心是Java实体类属性名和数据表字段名映射没对齐。如果你没有开map-underscore-to-camel-case,那么owner_id无法自动转成ownerId查询结果里这个字段一定是null。开了自动映射后仍然为null就要检查实体类里是否用了基本类型int而查询结果是NULLMyBatis反射注入时直接把int设为0而不是null。表设计时对可空字段尽量用包装类型Integer否则你无法区分“没数据”和“数值为0”。还有一种情况是resultType写了全限定类名但实体类里字段名和setter名不一致Lombok的Data生成的setter会遵循JavaBean规范字段名的第二个字母如果是大写比如aVar生成的setter就是setAVarMyBatis匹配时就会失败。这个细节几乎没有资料会提醒我调试过整整半天。5.5 现象定时提醒任务执行了多次或者根本没有执行疫苗提醒任务有时会触发重复执行。原因要么是项目部署了多个实例每个实例都在扫描同一张表同一条记录被发了两次提醒要么是上一次任务执行时间过长进入下一次扫描时上一次还没结束Scheduled默认单线程下其实会排队但如果你配了线程池它们就并行跑了同时扫到同一条记录。解决的思路是先让扫描条件带上“已提醒”标记比如在health_record表里加一列reminded_flag任务处理完置为1下次扫描条件加上reminded_flag 0。这个方案简单可靠比分布式锁的复杂度低很多。5.6 现象项目本地能跑部署到Linux服务器却频繁报数据库连接超时开发环境的MySQL和应用在同一台机器网络延迟低而服务器上两者可能跨机房。HikariCP默认的连接超时是30秒如果数据库服务器负载高连接池的连接建立容易超时。碰到这个情况我会在配置里把connection-timeout调到3000validation-timeout稍微放宽同时确认MySQL的max_connections是不是太小。数据库连接池并不是越大越好比如4核8G的机器连接池设20到30就够了设成200反而会拖垮数据库这也是很多人常说的“连接池参数玄学”。5.7 现象页面照常渲染但状态更新后列表不刷新最后一种常见问题是前端缓存。很多宠物管理系统没有做前后端分离直接使用Thymeleaf或JSP模板浏览器会把静态资源缓存在本地更新部署后用户看到还是旧页面。我处理后端更新时会顺手改一下模板文件里的版本参数比如main.css?v20250320或者干脆后端接口返回列表时加一个Cache-Control: no-cache响应头。这个不算后端bug但是外包验收时客户最容易发现并投诉。6. 从能跑通到能上线接口测试、部署验证和代码交付的三个经验系统写完之后我会按一个固定流程做验收顺序是接口层测试 - 业务逻辑测试 - 部署冒烟测试。接口层测试用Postman或者直接写JUnit。建议把每一个POST接口都检查一遍参数校验是否生效比如宠物重量传负数、生日传未来日期、订单金额传字符串这些边界值如果后端没有校验前端页面上虽然写死了输入框但接口被脚本调用时会漏出很多问题。Spring Boot里用Valid注解加javax.validation包能快速给实体类字段加上NotBlank、DecimalMin这些规则不用手工写大量if判断。对于宠物管理系统的核心链路——用户注册、添加宠物、预约服务、录入疫苗、触发提醒要准备一组业务数据从头到尾跑一遍。最好写一个集成测试类用SpringBootTest把整个链路串起来跑。这一步能发现数据库事务问题比如Service方法如果忘了加Transactional插入宠物档案成功后插入健康记录失败数据就出现脏表。部署前的检查清单我会逐项核对配置分离本地开发和线上配置分开至少用spring.profiles.active区分dev和prod密码不要硬编码在配置文件里。日志策略logback配置里把ERROR级别日志单独输出到文件线上排错时才不至于在大量INFO日志里翻找。数据库备份上线前导出一次完整SQL备份放在部署目录之外的路径。Nginx反向代理如果用了Nginx转发请求注意设置client_max_body_size否则宠物图片超过默认1MB就直接被Nginx拦截后端根本没收到请求。java -jar pet-system.jar --server.port8080 --spring.profiles.activeprod --upload.dir/data/pet/uploads/最后给大家一个我自己的执拗习惯每次改完数据库表结构都会在Git仓库里保留一份对应的DDL变更脚本文件名按日期和修改内容命名比如20250615_add_reminded_flag.sql。这样做的好处是两个多月后客户要加一个“宠物美容积分”功能你还能记得当初的字段是怎么设计的不用对着数据库猜。老项目人员变动大的时候这套习惯能救命。希望这个宠物管理系统的实现思路对你有所帮助。本文还有配套的精品资源点击获取
返回列表