简介:本资源是一套已通过导师验收的高分本科毕业设计项目——基于SpringBoot开发的宠物医院寄养管理系统,面向计算机类专业本科生、Java初学者及课程设计实践者,解决宠物寄养业务中客户预约、宠物信息登记、订单管理、员工协同等核心场景的信息化需求。压缩包共207个文件,含113个Java后端逻辑类、26个HTML前端页面(如pet_boarding_table.html、personal_order_inquiry_admin.html)、19个XML配置与Mapper文件、16个PNG/9个JPG界面素材,以及SQL数据库脚本、application.yml配置、使用手册.docx等关键交付物,整体13.19MB,开箱即用。已有1436人学习下载,资源结构规范,模块划分清晰(涵盖用户、宠物、订单、商品、寄养管理等完整业务域),附带可直接运行的mvnw构建脚本与配套说明文档,省去环境适配与基础功能开发时间,显著提升毕设推进效率与答辩通过率。
1. 这不是又一个CRUD模板:SpringBoot宠物医院寄养系统的真实业务切口在哪?
很多学生拿到“宠物医院寄养管理系统”源码第一反应是——又是用户+订单+商品三件套?但拆开这个已通过导师验收的毕设项目,会发现它卡在了一个真实业务断层上:宠物寄养场景中“人-宠-时间-空间”的强耦合调度逻辑。它没用泛泛的预约表,而是用pet_boarding_table(宠物寄养单)和personal_order_inquiry(个人订单查询)两个核心页面,把“某只金毛犬在3月15日—3月20日入住B区3号笼位、每日喂食两次+遛放一次+驱虫记录”这种颗粒度的操作固化进数据库字段;shopping_commodity.html表面是商品页,实则对接寄养期间的增值服务(如洗澡、美毛、疫苗补打),订单与寄养单通过boarding_id关联而非简单外键。这意味着,你跑通它不只是学会SpringBoot增删改查,而是理解如何用Java Web建模“活体服务交付”——宠物不说话,但系统必须替它记住体温、进食量、排泄频次等非结构化数据的结构化落库方式。适合正在写毕设却卡在“业务深度不够被导师打回”的计算机专业学生,也适合想用真实场景练手MyBatis动态SQL与事务传播控制的Java初学者。
2. 从数据库脚本到SpringBoot启动:环境链路验证的四个关键断点
2.1 数据库初始化:看清建表语句里的业务隐喻
项目附带的SQL脚本(通常命名为schema.sql或init.sql)不是标准ER图产物。重点看三张表:
-- pet_boarding_table:寄养主表,注意status字段枚举值 CREATE TABLE pet_boarding_table ( id BIGINT PRIMARY KEY AUTO_INCREMENT, pet_name VARCHAR(50) NOT NULL, -- 宠物名(非主人名) boarding_start DATE NOT NULL, -- 入住起始日(DATE类型,非DATETIME) boarding_end DATE NOT NULL, -- 离院截止日(用于计算天数) cage_location VARCHAR(20), -- 笼位编号(B区3号→直接存字符串,非关联cage表) status ENUM('CHECK_IN','ON_BOARDING','CHECK_OUT','CANCELLED') DEFAULT 'CHECK_IN', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- personal_order_inquiry:个人订单查询视图或表,含冗余字段提升查询效率 CREATE TABLE personal_order_inquiry ( order_id BIGINT, pet_name VARCHAR(50), service_type VARCHAR(20), -- 'BOARDING','BATHING','VACCINATION' amount DECIMAL(10,2), order_date DATE, boarding_id BIGINT -- 显式关联寄养单,支持跨服务追溯 ); -- shopping_commodity:商品表,price字段带小数,但unit字段限定为'次'/'瓶'/'份' CREATE TABLE shopping_commodity ( id BIGINT PRIMARY KEY, name VARCHAR(100), price DECIMAL(8,2) NOT NULL, -- 精确到分,非FLOAT unit ENUM('次','瓶','份','小时') DEFAULT '次', category ENUM('SERVICE','PRODUCT') -- 区分服务类(遛狗)与实物类(驱虫药) );提示:
boarding_end用DATE而非DATETIME,是因为寄养结算按自然日计费(3月15日00:00至3月20日24:00=6天),避免时区和精度干扰;cage_location直接存字符串而非外键,是因笼位编号规则(A/B/C区+数字)常变动,硬关联反而增加维护成本。
2.2 SpringBoot配置校验:application.yml中的三个易错参数
项目根目录下application.yml文件需重点核对以下配置,缺一不可:
spring: datasource: url: jdbc:mysql://localhost:3306/pet_hospital?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: validate # 关键!设为validate而非update,防止误删生产数据 show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect # MyBatis配置显式声明,避免自动扫描失败 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 静态资源路径映射,匹配项目中的HTML文件位置 spring: mvc: static-path-pattern: /static/** resources: static-locations: classpath:/static/,classpath:/public/注意:
ddl-auto: validate是毕设项目安全底线——它只校验实体类与表结构是否一致,不执行ALTER操作;若启动报Table 'xxx' doesn't exist,说明SQL脚本未执行或数据库名不匹配;mapper-locations必须指向src/main/resources/mapper/下的XML文件,该路径在源码中对应pet_boarding_table_admin.html等页面的后端接口映射。
2.3 启动类与依赖验证:mvnw.cmd背后的Maven版本陷阱
项目含mvnw.cmd(Windows)和mvnw(Mac/Linux),说明使用Maven Wrapper。执行前先验证:
# 在项目根目录执行(非IDE内Terminal) ./mvnw -v # Linux/Mac mvnw.cmd -v # Windows预期输出应含Apache Maven 3.8.6或更高(SpringBoot 2.7.x要求Maven ≥3.5)。若报错Unsupported major.minor version 61.0,说明JDK版本过高(Java 17编译,但本地JDK是11),需统一为JDK 11(项目.java-version或pom.xml中<java.version>11</java.version>已锁定)。
关键依赖检查(pom.xml片段):
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <!-- 注意:非3.x,避免WebMvcConfigurer变更导致HTML静态资源404 --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <!-- 页面渲染核心 --> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> <!-- 适配SpringBoot 2.7.x --> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>提示:Thymeleaf版本必须与SpringBoot父POM兼容,若页面显示空白且控制台无错误,大概率是
spring-boot-starter-thymeleaf版本不匹配导致模板引擎未加载。
2.4 首页访问链路诊断:从URL到HTML文件的物理路径映射
启动成功后访问http://localhost:8080/,若返回404,按顺序排查:
- 确认静态资源路径:项目中
personal_order_inquiry.html位于src/main/resources/static/下(非templates/),因为它是纯前端页面,由Spring MVC直接返回,不经过Thymeleaf解析; - 检查Controller映射:查看
com.example.pet.controller.IndexController.java是否存在:
@Controller public class IndexController { @GetMapping("/") public String index() { return "redirect:/personal_order_inquiry.html"; // 重定向到具体页面 } }- 验证HTML文件名一致性:
personal_order_inquiry.html与personal_order_inquiry_admin.html是两个独立页面,前者面向客户,后者面向管理员,URL路径必须完全匹配文件名(大小写敏感); - 浏览器开发者工具Network标签:刷新页面,看
personal_order_inquiry.html请求状态码。若为404,检查src/main/resources/static/目录下是否存在该文件(注意Windows隐藏扩展名可能导致.html.txt误判)。
3. 核心业务功能实战:寄养单创建与状态流转的代码级实现
3.1 寄养单创建流程:从HTML表单到数据库插入的全链路
pet_boarding_table.html是寄养单录入页,其表单提交指向/boarding/save:
<!-- pet_boarding_table.html 片段 --> <form th:action="@{/boarding/save}" method="post"> <input type="text" name="petName" required /> <input type="date" name="boardingStart" required /> <input type="date" name="boardingEnd" required /> <select name="cageLocation"> <option value="A区1号">A区1号</option> <option value="B区3号">B区3号</option> </select> <button type="submit">提交寄养</button> </form>后端Controller接收并校验:
@PostMapping("/boarding/save") public String saveBoarding(@Valid BoardingForm form, BindingResult result, Model model) { if (result.hasErrors()) { model.addAttribute("errors", result.getAllErrors()); return "pet_boarding_table"; // 返回原页面并显示错误 } // 业务校验:入住日不能晚于离院日 if (form.getBoardingStart().isAfter(form.getBoardingEnd())) { model.addAttribute("error", "入住日期不能晚于离院日期"); return "pet_boarding_table"; } // 计算寄养天数(含首尾日) long days = ChronoUnit.DAYS.between(form.getBoardingStart(), form.getBoardingEnd()) + 1; BoardingEntity entity = new BoardingEntity(); entity.setPetName(form.getPetName()); entity.setBoardingStart(form.getBoardingStart()); entity.setBoardingEnd(form.getBoardingEnd()); entity.setCageLocation(form.getCageLocation()); entity.setStatus("CHECK_IN"); // 初始状态为已入住 entity.setDays(days); // 天数预计算,避免每次查询都计算 boardingService.save(entity); return "redirect:/pet_boarding_table_admin.html"; // 跳转管理页 }逻辑说明:
ChronoUnit.DAYS.between返回的是两个日期间的完整日历日差(3月20日减3月15日=5),加1才是实际寄养天数(15、16、17、18、19、20共6天);@Valid触发JSR-303校验,BindingResult捕获校验失败信息,避免异常中断流程。
3.2 状态机驱动的寄养生命周期:MyBatis动态SQL实现多条件更新
寄养单状态流转(CHECK_IN → ON_BOARDING → CHECK_OUT)由管理员在pet_boarding_table_admin.html操作。关键在于BoardingMapper.xml中的动态更新:
<!-- src/main/resources/mapper/BoardingMapper.xml --> <update id="updateStatusById"> UPDATE pet_boarding_table SET status = #{status}, updated_at = NOW() WHERE id = #{id} <if test="status == 'CHECK_OUT'"> AND status IN ('CHECK_IN', 'ON_BOARDING') <!-- 防止跳过中间状态直接离院 --> </if> <if test="status == 'ON_BOARDING'"> AND status = 'CHECK_IN' <!-- 只能从入住态进入在院态 --> </if> </update>对应的Service方法:
@Service public class BoardingService { @Transactional public void updateStatus(Long id, String status) { // 先查当前状态做二次校验(弥补SQL层面的不足) BoardingEntity current = boardingMapper.findById(id); if (current == null) throw new RuntimeException("寄养单不存在"); // 状态迁移合法性检查 if ("CHECK_OUT".equals(status) && !Arrays.asList("CHECK_IN", "ON_BOARDING").contains(current.getStatus())) { throw new RuntimeException("无法从当前状态" + current.getStatus() + "直接离院"); } if ("ON_BOARDING".equals(status) && !"CHECK_IN".equals(current.getStatus())) { throw new RuntimeException("只能从入住态进入在院态"); } boardingMapper.updateStatusById(id, status); } }参数说明:
<if>标签确保SQL层面的状态约束,@Transactional保证状态更新与可能的关联操作(如生成结算单)原子性;二次校验在Service层,因MyBatis动态SQL无法捕获“无行更新”异常(即WHERE条件不匹配时返回0),需主动查库确认。
3.3 个人订单查询:跨表关联与分页的MyBatis实现
personal_order_inquiry.html需展示用户所有订单(含寄养、洗澡、疫苗),后端通过OrderMapper.xml关联查询:
<select id="findOrdersByUserId" resultType="map"> SELECT o.id as order_id, p.pet_name, o.service_type, o.amount, o.order_date, b.cage_location, b.boarding_start, b.boarding_end FROM personal_order_inquiry o LEFT JOIN pet_boarding_table b ON o.boarding_id = b.id LEFT JOIN pet_info p ON b.pet_id = p.id <!-- 假设存在pet_info表 --> WHERE o.user_id = #{userId} ORDER BY o.order_date DESC LIMIT #{offset}, #{limit} <!-- 物理分页,非内存分页 --> </select>Controller中分页参数处理:
@GetMapping("/order/inquiry") public String inquiryOrders(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, Model model) { Long userId = getCurrentUserId(); // 从Session获取当前登录用户ID int offset = (page - 1) * size; List<Map<String, Object>> orders = orderService.findOrdersByUserId(userId, offset, size); int total = orderService.countOrdersByUserId(userId); model.addAttribute("orders", orders); model.addAttribute("currentPage", page); model.addAttribute("totalPages", (int) Math.ceil((double) total / size)); return "personal_order_inquiry"; }提示:
LIMIT #{offset}, #{limit}是MySQL特有语法,若切换Oracle需改为ROWNUM子句;Math.ceil计算总页数时强制转double,避免整数除法截断(如15条数据/每页10条=1页,实际需2页)。
4. 管理员后台与权限隔离:基于Session的简易角色控制实现
4.1 登录认证流程:从login.html到Session存储的轻量级方案
项目未集成Spring Security,采用手动Session管理。login.html表单提交至/login:
<form action="/login" method="post"> <input type="text" name="username" placeholder="管理员账号" required /> <input type="password" name="password" placeholder="密码" required /> <button type="submit">登录</button> </form>Controller处理逻辑:
@PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) { // 简单明文比对(毕设可接受,生产环境必须加密) if ("admin".equals(username) && "123456".equals(password)) { request.getSession().setAttribute("admin", true); // 设置Session标识 request.getSession().setAttribute("username", username); return "redirect:/pet_boarding_table_admin.html"; } else { return "login?error=true"; // 返回登录页并带错误参数 } }注意:
request.getSession()默认创建新Session,setAttribute将布尔值存入,后续所有管理页Controller需校验此Session属性。
4.2 管理页面拦截:HandlerInterceptor实现统一权限过滤
创建AdminInterceptor类:
@Component public class AdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session == null || session.getAttribute("admin") == null) { response.sendRedirect(request.getContextPath() + "/login.html"); return false; // 拦截请求 } return true; // 放行 } }注册拦截器(WebConfig.java):
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private AdminInterceptor adminInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(adminInterceptor) .excludePathPatterns("/login.html", "/login", "/static/**"); // 放行登录页和静态资源 } }提示:
excludePathPatterns必须包含/static/**,否则pet_boarding_table_admin.html的CSS/JS文件会被拦截,导致页面样式错乱;getSession(false)参数为false表示不创建新Session,避免未登录用户触发Session创建。
4.3 管理员专属操作:笼位占用校验的实时性保障
pet_boarding_table_admin.html中新增寄养单时,需校验所选笼位在指定日期是否已被占用。后端提供AJAX接口:
@GetMapping("/cage/check") @ResponseBody public Map<String, Object> checkCageAvailability( @RequestParam String cageLocation, @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate, @RequestParam(required = false) Long excludeId) { // 排除自身ID(编辑时) // 查询该笼位在日期范围内是否有其他有效寄养单 long count = boardingMapper.countConflictingBookings(cageLocation, startDate, endDate, excludeId); Map<String, Object> result = new HashMap<>(); result.put("available", count == 0); result.put("conflictCount", count); return result; }对应Mapper XML:
<select id="countConflictingBookings" resultType="long"> SELECT COUNT(*) FROM pet_boarding_table WHERE cage_location = #{cageLocation} AND status IN ('CHECK_IN', 'ON_BOARDING') AND boarding_start <= #{endDate} AND boarding_end >= #{startDate} <if test="excludeId != null"> AND id != #{excludeId} </if> </select>逻辑说明:
boarding_start <= endDate AND boarding_end >= startDate是区间重叠的经典判断(两区间不重叠当且仅当A.end < B.start OR A.start > B.end);status IN ('CHECK_IN','ON_BOARDING')过滤已离院或取消的单据,确保只校验有效占用。
5. 毕设答辩高频问题应对:三个必须掌握的技术细节与演示技巧
5.1 数据库设计答辩话术:为什么不用外键而用字符串存笼位?
当导师问“笼位编号为什么不建cage表并设外键?”时,回答要聚焦业务演进:
“因为宠物医院的笼位物理布局常调整——比如B区3号笼位可能因维修临时关闭,或A区扩容新增A区10号。若用外键关联cage表,每次调整都要维护cage表数据,且历史寄养单会因cage记录删除而失效。而字符串存储(如‘B区3号’)让笼位编号成为寄养单的快照属性,既保留历史准确性,又避免级联删除风险。我们在
pet_boarding_table_admin.html的笼位筛选功能中,用JavaScript动态生成下拉选项,数据来自/cage/list接口,该接口实时查询当前有效笼位,实现逻辑与物理的解耦。”
5.2 SpringBoot配置答辩演示:如何证明application.yml生效?
现场演示时,不要只说“我配了”,要给出可验证证据:
- 启动日志搜索
HikariCP连接池信息,确认jdbc:mysql://localhost:3306/pet_hospital出现在日志中; - 在Controller中注入
Environment,添加测试接口:
@GetMapping("/config/test") @ResponseBody public String testConfig(@Autowired Environment env) { return "DB URL: " + env.getProperty("spring.datasource.url") + ", JPA DDL: " + env.getProperty("spring.jpa.hibernate.ddl-auto"); }访问http://localhost:8080/config/test,返回值应与application.yml完全一致; 3. 故意将ddl-auto改为create-drop,重启观察控制台是否打印建表SQL——这证明配置被加载且生效。
5.3 业务逻辑答辩亮点:寄养天数计算的边界案例处理
准备一个具体案例演示:“客户3月31日入住,4月3日离院,系统计算为4天还是5天?”
在BoardingService中定位计算逻辑:
// 正确实现(含边界) public long calculateDays(LocalDate start, LocalDate end) { return ChronoUnit.DAYS.between(start, end) + 1; // 31→1→2→3 = 4天 }对比错误实现(常见坑):
// 错误:end.minus(start).getDays() → 4月3日减3月31日=3天(漏首日) // 错误:end.getDayOfYear() - start.getDayOfYear() + 1 → 跨年失效演示时打开
pet_boarding_table.html,输入3月31日和4月3日,提交后在pet_boarding_table_admin.html查看生成的days字段值为4,并解释:“寄养计费按自然日,3月31日当天算第1天,4月3日当天算第4天,系统用ChronoUnit.DAYS精确计算日历差,加1确保首尾日均计入。”
最后,在pet_boarding_table_admin.html的“导出Excel”按钮旁,添加一个隐藏的调试开关:按住Ctrl+Alt点击,弹出当前登录用户Session信息及数据库连接状态——这个小技巧能让答辩时快速验证环境真实性,比口头描述更有力。
本文还有配套的精品资源,点击获取