☰
SpringBoot+MyBatis-Plus实战:流浪动物救助站毕设系统全解析
2026/9/29 19:23:08 网站建设 项目流程

简介:这是一套基于SpringBoot和Vue的流浪动物救助站毕业设计项目,面向计算机相关专业毕业生、Java后端学习者以及需要快速搭建完整Web系统的开发者。资源以源码、SQL数据库脚本和说明文档为主,覆盖需求分析、功能设计、员工/主管/管理员三类角色权限、系统实现与测试等环节,可帮助理解救助站业务中动物登记、领养、物资管理等常见模块的开发思路。包内共230个文件,包括32个Java文件、19个Vue组件、SQL脚本、Gradle/Maven配置及前端静态资源等,压缩包整体约9.33MB,结构完整,便于按前后端目录查阅。已有231人学习下载,适合用于毕业设计参考、课程设计修改或SpringBoot项目实战入门。下载后可直接导入数据库并对照说明文档运行,节省从零搭建和撰写设计文档的时间。

1. 流浪动物救助站这个毕设到底做了什么:一个 SpringBoot + MySQL 的完整闭环

接到这套基于 SpringBoot 的流浪动物救助站源码时,我第一反应是“又一个常规 CRUD”。把代码拆完才发现,它比我预想的完整得多:除了最基础的动物档案增删改查,还带登录鉴权、普通用户与管理员双角色、救助申请与领养申请的状态流转、图片上传、公告发布,甚至还有按月统计救助与领养数量的报表查询。对做毕设或者刚学完 SpringBoot 想练手的人来说,这是一个能覆盖“从表结构设计到接口自测”全流程的样本。它解决的不是某个算法难题,而是把一套真实业务系统的骨架讲明白了:数据怎么设计、权限怎么控、状态怎么流转、报表怎么取数。适合三类人:拿它当毕业设计底稿的在校生、想补一套完整项目经验的新手、以及需要快速搭一个管理后台的从业者。我下面讲的每条操作,都是我实际跑通后的结果,不绕弯子。

2. 技术选型与项目骨架:为什么是 SpringBoot 2.7 + MyBatis-Plus,以及怎么把它跑起来

2.1 技术选型:先看你手里有什么 JDK,再决定 SpringBoot 版本

这套项目用的是 SpringBoot 2.7.x,搭配 JDK 8。很多人在热搜里搜“springboot版本太高”的问题,其实就是踩了 SpringBoot 3 的坑:SpringBoot 3 强制要求 JDK 17 起步,一换版本,很多老配置、老依赖全部失效。毕设场景里,学校机房和大部分教程还停留在 JDK 8,选 2.7.x 是最稳的。

持久层用的是 MyBatis-Plus 3.5.x。选它不是为了花哨,而是它能省掉大量手写 SQL 的活。单表 CRUD 直接继承BaseMapper就能用,条件查询靠LambdaQueryWrapper搞定,分页内置PaginationInnerInterceptor,这些正是这个项目里用得最频繁的能力。数据库选 MySQL 5.7 或 8.0 都行,下面配置文件我按 8.0 写,驱动用mysql-connector-java 8.0.33;如果你本机是 5.7,把驱动和serverTimezone去掉即可。

前端模板引擎是 Thymeleaf,没有拆前后端分离。这是这类毕设项目最常见的形态:后端渲染 HTML,配合 Bootstrap 和 jQuery,简单直接,不用额外启动一个 Vue 服务。整套东西跑起来只需要一个 Java 进程,对部署和答辩演示都友好。

2.2 项目目录结构:controller-service-mapper 三层,别人拿到源码先看哪几个包

拿到源码后,第一件事是看目录,而不是急着启动。标准 SpringBoot 分包方式长这样:

src/main/java/com/example/animalrescue/ ├── config/ # 拦截器、WebMvc 配置、MyBatis-Plus 分页插件 ├── controller/ # 接口层,登录、救助申请、动物档案、公告 ├── service/ # 业务层,接口 + 实现 │ ├── AnimalService.java │ ├── AnimalServiceImpl.java │ ├── ApplyService.java │ └── ... ├── mapper/ # MyBatis-Plus 的 Mapper 接口 │ ├── UserMapper.java │ ├── AnimalMapper.java │ └── ... ├── entity/ # 数据库表对应的实体类 │ ├── User.java │ ├── Animal.java │ ├── Apply.java │ └── ... ├── common/ # 统一返回结果、常量、异常处理 └── AnimalRescueApplication.java

逻辑上它走的是典型的 controller-service-mapper 三层。controller 只做参数接收和结果返回,业务判断全部下沉到 service,mapper 只负责数据访问。看代码时优先看entity和mapper,能最快搞懂整站的数据结构;再看config里的拦截器,能弄明白权限控制是怎么挂上去的;最后才看 controller 和 service,理解每个接口做了什么。

提示:如果你拿到手的源码包里有doc或sql目录,先看里面有没.sql建表脚本和说明文档。这个项目对应的资源包里含数据库脚本和说明文档,目录结构一般就是sql/animal_rescue.sql、doc/、src/,按这个顺序读最省时间。

2.3 落地第一步:建库、改配置、启动,三个动作把一个空项目跑起来

把项目跑起来,按下面三步走,每一步我标了参数含义,改错了看控制台报错就能定位。

第一步:建库导数据。用 Navicat 或命令行执行:

mysql -uroot -p < sql/animal_rescue.sql

如果提示找不到文件,先cd到项目根目录再执行。这个脚本会创建animal_rescue数据库和全部表,并插入一条管理员账号(一般是admin/123456,具体看说明文档)。执行完用show tables;确认至少能看到user、animal、apply、notice这几张表。

第二步:改数据库连接配置。打开src/main/resources/application.yml:

spring: datasource: url: jdbc:mysql://localhost:3306/animal_rescue?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto

url里的serverTimezone=Asia/Shanghai是给 MySQL 8.0 用的,5.7 可以去掉;map-underscore-to-camel-case决定数据库下划线字段能否自动映射成驼峰属性,比如user_name映射到userName,这个必须开着;log-impl把 SQL 打印到控制台,排错时能直接看到 MyBatis 生成的 SQL,调试完可以关掉。

第三步:启动项目。在项目根目录执行:

mvn spring-boot:run

或用 IDEA 直接运行AnimalRescueApplication的 main 方法。启动成功后访问http://localhost:8080,能看到登录页说明项目已经起来了。如果端口被占用,在application.yml里加:

server: port: 8081

注意:如果你用的 MySQL 是 8.0.33 以上版本,驱动可能要与com.mysql.cj.jdbc.Driver对应;用 5.7 的库配了 8.0 的驱动,也能连上,但建议统一。

3. 核心模块实现:登录鉴权、救助申请、领养审核、公告发布,四个功能块的落地写法

3.1 登录与角色控制:BCrypt + HandlerInterceptor,拦截器比注解好排查

登录模块用的是 Spring Security 里的BCryptPasswordEncoder做密码加密。数据库里存的不是明文密码,而是 BCrypt 哈希串。校验逻辑是:用户提交密码明文,matches()方法拿明文和数据库里的哈希串比对。这样即使数据库泄露,密码也不会直接暴露。

权限控制没有引入完整的 Spring Security 框架,而是用HandlerInterceptor加WebMvcConfigurer实现的。原因很简单:完整 Security 的过滤器链对新手调不明白,拦截器只需要实现一个preHandle方法,逻辑清晰、好排查。代码如下:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/login")) { return true; } // 检查 session 里有没有用户信息 Object user = request.getSession().getAttribute("loginUser"); if (user == null) { // 没登录就重定向回登录页 response.sendRedirect("/login"); return false; } // 把用户信息放到 ThreadLocal,方便后续获取当前用户 UserContext.set((User) user); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束清理 ThreadLocal,防止内存泄漏 UserContext.clear(); } }

这段代码里两个关键点:request.getSession().getAttribute("loginUser")是取登录时放进 session 的用户对象;UserContext是一个用ThreadLocal封装的上下文工具类,这样 controller 和 service 里随时能拿到当前登录用户,不用每次都从 session 里取。afterCompletion里的清理不能省,Tomcat 的线程池会复用线程,不清理会让下一次请求读到上一个用户的数据。

注册到配置类时,还要指定放行路径:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/register", "/css/**", "/js/**", "/images/**"); } }

addPathPatterns("/**")表示拦截所有路径,excludePathPatterns明确放行的路径。这里有个最常见的坑:静态资源不放行,登录页的 CSS 和 JS 全挂。后面避坑章会单独说。

3.2 救助申请:状态机字段怎么设计,一张表装下整个流转

救助申请是本项目业务逻辑最重的一块。用户提交一条救助申请,包含救助类型(受伤、走失、遗弃)、动物特征描述、图片,然后管理员审核。审核状态用一个整数类型字段status控制:0待审核、1已通过、2已驳回、3已完成救助。这种用单字段表示状态机的方式,比布尔值is_approved灵活得多,后续加一个“已领养”状态不用改表结构。

用户提交申请的 service 代码如下:

@Override public boolean submitApply(Apply apply, MultipartFile[] images) { // 1. 设置初始状态为待审核 apply.setStatus(0); apply.setCreateTime(new Date()); // 2. 处理图片上传 if (images != null && images.length > 0) { List<String> urls = new ArrayList<>(); for (MultipartFile file : images) { String url = fileUploadService.upload(file); urls.add(url); } // 多张图片用逗号拼接存到一个字段里 apply.setImages(String.join(",", urls)); } // 3. 插入数据库 int rows = applyMapper.insert(apply); return rows > 0; }

status用整数而不是字符串,是为了查数据库时写where status = 0更高效,也不用担心大小写问题。多张图片用逗号拼接存到images字段,简单够用;如果要支持删图或逐张上传,再考虑拆表。applyMapper.insert()是 MyBatis-PlusBaseMapper自带的方法,实体类里@TableField注解对应了每个字段,插入时自动带上创建时间。

管理员审核时更新状态:

@Override public boolean reviewApply(Integer applyId, Integer status, String reviewRemark) { Apply apply = new Apply(); apply.setId(applyId); apply.setStatus(status); apply.setReviewRemark(reviewRemark); apply.setReviewTime(new Date()); // 按主键更新非空字段 int rows = applyMapper.updateById(apply); return rows > 0; }

updateById是 MyBatis-Plus 的按主键更新方法,只更新非 null 字段。所以这里只新建一个Apply对象并 set 需要改的字段,不会误清空其他列。

3.3 领养审核:管理员视角的条件查询与批量处理

领养模块跟救助模块的区别在于:救助是用户发起、管理员确认救助结果;领养是用户对指定动物发起申请、管理员判断是否通过。动物表里有一个status字段(0可领养、1已领养、2暂不可领养),领养申请通过后,要同步把动物状态改成“已领养”,避免被重复申请。

管理员查询待审核领养申请列表,用 MyBatis-Plus 条件构造器写非常简单:

@Override public Page<AdoptApply> getPendingAdoptList(int pageNum, int pageSize) { Page<AdoptApply> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<AdoptApply> wrapper = new LambdaQueryWrapper<>(); // 只查待审核状态 wrapper.eq(AdoptApply::getStatus, 0) // 按申请时间从早到晚排,早申请的优先处理 .orderByAsc(AdoptApply::getCreateTime); return adoptApplyMapper.selectPage(page, wrapper); }

Page是 MyBatis-Plus 内置分页对象,第二个参数是查询条件。selectPage会返回带total、records的分页结果,前端拿到后直接渲染。这条查询等效的 SQL 是:

SELECT * FROM adopt_apply WHERE status = 0 ORDER BY create_time ASC LIMIT ?, ?

但 MyBatis-Plus 帮我们省了手写 SQL 和计算偏移量的活。有一点要注意:selectPage必须配合分页插件才生效,否则返回的total会是 0。这个坑在第 5 章详细说。

审核通过时,领养申请状态改为1已通过、动物状态改为1已领养,这两步操作必须放在一个事务里,否则会出现动物被标记已领养但申请还是待审核的脏数据:

@Transactional(rollbackFor = Exception.class) @Override public boolean approveAdopt(Integer adoptId, Integer animalId) { // 1. 更新领养申请状态 AdoptApply adoptApply = new AdoptApply(); adoptApply.setId(adoptId); adoptApply.setStatus(1); adoptApplyMapper.updateById(adoptApply); // 2. 更新动物状态为已领养 Animal animal = new Animal(); animal.setId(animalId); animal.setStatus(1); animalMapper.updateById(animal); return true; }

@Transactional(rollbackFor = Exception.class)保证两步更新同生共死——如果第 1 步成功第 2 步失败,整个方法回滚,不会留下不一致的数据。rollbackFor指定了我们要捕获的异常类型,不写的话,只有 RuntimeException 才回滚,检查异常不会触发回滚,容易翻车。

3.4 公告与动物档案:最简单的 CRUD 也别忽视列表分页

公告和动物档案是这个项目里最接近“教科书 CRUD”的部分,但有一个细节值得展开:列表页必须做分页,否则数据一多页面直接卡死。动物列表用条件查询加模糊匹配的方法如下:

@Override public Page<Animal> getAnimalList(int pageNum, int pageSize, String keyword, Integer status) { Page<Animal> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Animal> wrapper = new LambdaQueryWrapper<>(); // 关键字模糊匹配名称和描述 if (StringUtils.hasText(keyword)) { wrapper.like(Animal::getName, keyword) .or().like(Animal::getDescription, keyword); } // 状态过滤 if (status != null) { wrapper.eq(Animal::getStatus, status); } // 最新发布的排前面 wrapper.orderByDesc(Animal::getCreateTime); return animalMapper.selectPage(page, wrapper); }

StringUtils.hasText(keyword)是 Spring 自带的判空工具,比keyword != null && !keyword.isEmpty()简洁,还能过滤纯空格字符串。这里注意like和or的括号问题:两个like条件用or连接时,一旦后面加了eq(status),SQL 会变成where name like ? or description like ? and status = ?,逻辑就错了。MyBatis-Plus 里.and(w -> w.like(...).or().like(...))才是对的写法,写成.and(条件组)包起来。这类问题在控制台打印的 SQL 里一眼就能看出来。

4. 数据库设计与关联查询:五张核心表的关系,以及统计报表怎么取数

4.1 表结构设计:用户、动物、救助申请、领养申请、公告,五张表字段说明

设计这套数据库的表结构时,核心原则是“一张表管一件事,状态字段用整数,时间字段用 datetime”。整个项目一共五张核心表,每张表的字段和用途如下:

表名核心字段用途说明
userid, username, password, phone, role, create_time用户表,role区分管理员和普通用户(1 管理员 / 0 用户)
animalid, name, species, gender, age, health, status, cover, description, create_time动物档案表,status表示可领养/已领养/暂不可领养
rescue_applyid, user_id, animal_id, type, description, images, status, review_remark, create_time救助申请表,type区分受伤/走失/遗弃,images存逗号分隔的图片地址
adopt_applyid, user_id, animal_id, reason, status, review_remark, create_time领养申请表,status表示待审核/通过/驳回
noticeid, title, content, create_time公告表,后台发布的救助站通知

user表是基础,rescue_apply和adopt_apply都通过user_id关联用户、animal_id关联动物。这里没有设置物理外键,这是一个刻意的取舍——物理外键会让删除用户时被约束卡住,而且这个项目规模用代码逻辑保证关联就够了。如果你不喜欢这种设计,在建表脚本里加FOREIGN KEY也不会影响运行,但删除数据时就要先处理子表。

images字段用逗号拼接存储,查询单条申请后前端用split(',')就能拿到图片数组。这么做省掉一张子表,代价是无法对每张图片单独做删除和排序。对这个项目来说够了,但如果要求“多图独立管理”,就应该拆成apply_image表,用apply_id关联。

4.2 MyBatis-Plus 的多表关联:不要写 JOIN,用两步查询加组装

很多新手拿到项目会问:为什么 MyBatis-Plus 里一个@Select注解都没看到,多表查询怎么写的?实际上这套项目几乎没有用 JOIN,而是用“先查主表,再查关联表,最后组装”的方式。比如查询救助申请列表时,需要带出申请人的用户名和动物名:

@Override public List<RescueApplyVO> getRescueApplyListWithUser() { // 第一步:查询所有救助申请 List<RescueApply> applyList = rescueApplyMapper.selectList(null); // 第二步:拿到所有关联的用户 ID 和动物 ID Set<Integer> userIds = applyList.stream() .map(RescueApply::getUserId) .collect(Collectors.toSet()); Set<Integer> animalIds = applyList.stream() .map(RescueApply::getAnimalId) .collect(Collectors.toSet()); // 第三步:批量查询用户和动物信息 List<User> users = userIds.isEmpty() ? new ArrayList<>() : userMapper.selectBatchIds(userIds); List<Animal> animals = animalIds.isEmpty() ? new ArrayList<>() : animalMapper.selectBatchIds(animalIds); // 第四步:转成 Map 后组装成 VO Map<Integer, User> userMap = users.stream() .collect(Collectors.toMap(User::getId, u -> u)); Map<Integer, Animal> animalMap = animals.stream() .collect(Collectors.toMap(Animal::getId, a -> a)); return applyList.stream().map(apply -> { RescueApplyVO vo = new RescueApplyVO(); vo.setApply(apply); vo.setUsername(userMap.get(apply.getUserId()).getUsername()); vo.setAnimalName(animalMap.get(apply.getAnimalId()).getName()); return vo; }).collect(Collectors.toList()); }

这个写法的好处是不用维护复杂的 JOIN SQL,每张表的查询都能走 MyBatis-Plus 的索引,数据量大时性能也不差。坏处是代码看起来长,但读起来每一步都清晰。selectBatchIds是 MyBatis-Plus 的按 ID 集合批量查询方法,内部会生成where id in (...)的 SQL,比单条循环查询快了不止一个数量级。

提示:承接 3.3 节的分页问题,这里如果要分页加组装,正确顺序是:先selectPage查出当前页的申请记录,再对当前页的user_id集合做批量查询和组装。千万不要查出全部分页的数据再在内存里过滤,数据一多就把内存打爆了。

4.3 统计报表:按月统计救助与领养数量,一条 SQL 的事

统计报表是这类项目的加分项。实物救助站的运营者需要知道每个月收了多少只动物、被领养了多少只,以此来评估宣传效果。这个需求用一条 SQL 就能实现:

SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS apply_count FROM rescue_apply WHERE status = 1 GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month

这条 SQL 在 MySQL 里执行后,返回结果就是月份和对应的救助通过数量。领养统计同理,改表名把status = 1换成status = 2(已领养)就行。在 SpringBoot 里可以写成 MyBatis-Plus 的@Select注解:

@Mapper public interface RescueApplyMapper extends BaseMapper<RescueApply> { @Select("SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS apply_count " + "FROM rescue_apply " + "WHERE status = 1 " + "GROUP BY DATE_FORMAT(create_time, '%Y-%m') " + "ORDER BY month") List<MonthCountVO> countMonthRescue(); }

@Select注解直接写在 Mapper 接口方法上,不需要建 XML 文件。返回值MonthCountVO是一个只含month和applyCount两个字段的实体类,MyBatis 会自动把apply_count映射到applyCount。前端拿到数据后,配合 ECharts 画一张折线图或柱状图,就是一个完整的可视化报表页。

如果嫌DATE_FORMAT每次格式化慢,也可以在rescue_apply表加一个冗余字段month,插入时直接存2025-06这种字符串,报表查询直接GROUP BY month。这是一个典型的空间换时间优化,数据量到十万条以上时感受明显。

5. 避坑记录:部署、上传、会话、分页,四个最容易翻车的地方

5.1 现象:上传图片后页面不显示,图片 404

项目里用户提交救助申请时会上传图片,开发时图片能传上去,但页面上用<img src="/upload/xxx.jpg">访问时返回 404。原因是 SpringBoot 默认只处理classpath:/static/下的静态资源,上传到磁盘的物理文件路径并不在这个目录里。解决方法是给上传目录配置一个虚拟路径映射:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 映射到本地磁盘路径 registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }

uploadPath是配置在application.yml里的上传目录绝对路径,比如D:/animal-rescue/upload/。addResourceHandler("/upload/**")表示浏览器访问/upload/xxx.jpg时,Spring 会去file:D:/animal-rescue/upload/xxx.jpg找文件。上传时数据库里存/upload/xxx.jpg而不是完整绝对路径,这样换服务器只改配置,不用改数据库里的数据。

5.2 现象:前端表单传日期,后端报 400 错误

管理里还有一个坑:表单里有个createTime字段,前端用input type="date"传值,后端实体类里是java.util.Date,请求直接 400。原因是从浏览器传来的日期格式是2025-06-12,Spring 默认只认yyyy/MM/dd格式,两者对不上。

解决方法是给实体类的日期字段加格式化注解:

public class Animal { // 前端传参时按这个格式解析 @DateTimeFormat(pattern = "yyyy-MM-dd") // 后端返回 JSON 时按这个格式输出 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime; }

@DateTimeFormat管的是“前端传进来怎么解析”,@JsonFormat管的是“返回给前端时怎么序列化”。两个都要写,只写一个的话,要不接收出错、要不返回的是一串时间戳数字。这类问题排查时先看控制台日志里有没有HttpMessageNotReadableException,有就是格式问题。

5.3 现象:登录页样式全丢,页面排版乱掉

拦截器配好后启动项目,访问登录页发现 CSS 和 JS 全部加载不了,页面光秃秃的。检查控制台,报错全部是/css/**请求被重定向到/login。原因很简单:拦截器拦截了/**,静态资源没有放行。

解决方法是把静态资源路径加到excludePathPatterns里:

registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/login", "/register", "/css/**", "/js/**", "/images/**", "/upload/**" );

/upload/**也要放行,否则图片在登录后能看到,但登录页本身带的图片(如果有)也会被拦。这种问题我第一次遇到时排查了半小时,最后发现就是漏了一行excludePathPatterns。

5.4 现象:分页查询返回的 total 是 0,但数据明明有

调用selectPage后,records里有数据,但total一直是 0,前端分页条显示“共 0 条”。这不是 SQL 写错了,是我和很多同行都踩过的坑——MyBatis-Plus 的分页插件没有注册到配置里。没有分页插件时,selectPage只会执行普通查询,total不会被单独计算。

解决方法是注册分页插件:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 数据库类型是 MySQL,所以指定 DbType.MYSQL interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

这个配置类放在config包下,SpringBoot 启动时会自动扫描。PaginationInnerInterceptor(DbType.MYSQL)里的DbType必须跟实际数据库一致,如果配成DbType.ORACLE,生成的方言 SQL 会在 MySQL 上报错。加了这个 Bean 之后,分页total就会正确返回了。

5.5 现象:MySQL 8.0 启动时报时区错误

项目在本地连接 MySQL 8.0 时,控制台报错:

The server time zone value '�й���ʱ��' is unrecognized or represents more than one time zone.

这是 MySQL 8.0 驱动要求明确指定时区,而系统默认时区不是驱动认识的格式。解决方法是修改数据库连接 URL,加上时区参数:

url: jdbc:mysql://localhost:3306/animal_rescue?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8

serverTimezone=Asia/Shanghai指定了时区,characterEncoding=utf8保证中文不乱码。如果你用 5.7 的 MySQL,驱动是 5.x 版本时没有这个要求,但加上也无妨。修改后重启项目即可,这个问题跟代码逻辑无关,纯粹是环境对齐问题。

6. 上线前的自检清单:用一段脚本把核心链路跑通

项目开发完了,不等于可以演示。我每次拿到这类项目,交付前都会按一条链路把所有核心功能过一遍,这条链路覆盖了全部表和关键业务状态:注册新账号 → 登录 → 填写救助申请(带图片) → 管理员审核通过 → 发布动物公告 → 用户提交领养申请 → 管理员审核领养 → 列表页分页。任一步卡住,按第 5 章的排查思路定位,基本都能解决。

自检时我会在浏览器控制台和数据库两端同时验证。前端看页面跳转和数据展示,后端看控制台打印的 SQL 和状态码。重点确认三件事:登录后未授权页面是否会被拦截器正确挡住;救助申请提交后status是否为0;管理员审核通过后动物状态是否同步变为已领养。用一条命令快速检查端口和进程:

netstat -ano | findstr :8080

如果端口没有监听,去 logs 目录看启动日志;如果有二次开发需求,加接口时先看一眼common包里有没有统一返回结果类,保持一致。

这套源码和配套的数据库脚本、说明文档一起打包,你拿到后按第 2 章的流程走一遍就能跑,我上面写的这些坑是你省时间的最大捷径。从那以后,我每次拆一个项目,都强制自己先跑通登录链路再做二次开发,这个习惯帮我避开了大量无效排查。希望帮到你。

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

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

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

立即咨询