ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue流浪动物救助平台全栈开发实战解析

SpringBoot+Vue流浪动物救助平台全栈开发实战解析 开始我接手这个项目的时候第一反应是“又一个管理系统”但真正把需求捋完才发现流浪动物救助这个场景比想象中复杂得多。它既有普通政务系统的信息发布、用户管理又涉及领养流程的审批流转、志愿活动的报名统计、寻宠启事的时效匹配再加上图片上传、数据统计这些杂活如果一开始不在架构上留好余地后面每个迭代都会把自己坑进去。这套系统整体就是经典的B/S架构后端用SpringBoot提供RESTful API前端用Vue做单页应用数据库MySQL存业务数据持久层用MyBatis处理SQL。整套源码我自己完整跑通过从数据库建表到前后端联调再到打包部署都踩过一遍坑。这篇文章就把整个项目的设计思路、核心实现和实战中遇到的问题一次性说清楚适合正在做毕业设计、想入门Java全栈开发、或者准备接外包练手的开发者参考。1. 项目全景流浪动物救助平台到底需要做什么1.1 业务模块与功能边界拆解流浪动物救助平台表面看是“一个网站”但它内部其实是三类人、两个端的组合体。三类人是普通用户、救助站管理员、系统超级管理员两个端是面向公众的前台展示端和管理员使用的后台管理端。普通用户能做的事主要有五块浏览待领养宠物列表和详情提交领养申请查看申请进度发布和浏览寻宠启事发布后发现宠物时能标记为“已找到”报名参加救助站组织的线下志愿活动查看自己的活动记录在线捐款系统里要做捐款记录不需要真的接支付接口但数据流和流程要完整在个人中心维护自己的基本信息、收养记录、申请记录。后台管理员端做的事情就是把这些数据管起来宠物信息的增删改查和下架领养申请的审核批准或驳回寻宠启事的审核志愿活动的发布和报名统计捐款明细的管理公告的发布以及用户账号的禁用和启用。把模块拆到这个粒度之后项目边界就非常清晰了。我最初犯过一个典型错误看到一个“平台管理系统”的标题就想把所有功能都塞进去包括论坛、在线聊天、视频直播结果做到一半发现重点全跑偏了。这个项目最核心的难点是“领养状态流转”其他功能都是围绕它服务的。1.2 技术选型为什么是这套组合技术栈用的是SpringBoot 2.7.18 JDK 8 MySQL 5.7 MyBatis 1.3.2 Vue 2.6这套组合我建议新接触的人直接照抄不要一上来就追新。原因很简单SpringBoot 3.x强制要求JDK 17很多公司的生产机器还是JDK 8Vue 3的生态组件和教程质量虽然已经很成熟但Vue 2的Element UI组件库依然是快速搭建后台界面的最高效选择尤其对于单人全栈开发写起来真的省力气。选MyBatis而不是JPA或者MyBatis-Plus也是同样的逻辑。很多人问我为啥不用MyBatis-PlusCRUD能省一大半代码量。我的回答是学习阶段用原生MyBatis你能真正理解SQL是怎么映射成对象的参数是怎么绑定到XML的动态SQL的拼接逻辑是怎么工作的。这些基础掌握了回头用MyBatis-Plus就是降维打击。而且这个项目里宠物列表的多条件查询、领养记录的嵌套查询手写SQL想怎么写就怎么写不会遇到插件在某些特定场景下SQL生成不对付的尴尬。MySQL 5.7虽然老但它和SpringBoot 2.7的配合最稳定小内存服务器上跑起来很轻快。数据库是整套系统的地基地基别乱动项目就稳了一半。2. 后端设计从建表到核心接口的落地细节2.1 业务表怎么设计才能不返工六大核心表拆解数据库设计是整个项目的地基。一个救助平台的核心数据流是“用户 → 宠物 → 领养申请 → 审核结果 → 归档”围绕这条主线我把表拆成六张核心表和四张辅助表。用户表user用户表要覆盖普通用户和管理员两种角色所以字段里必须有role字段取值范围建议用1表示管理员、0表示普通用户后台用status字段控制账号状态0为正常1为禁用。密码存储用MD5加密再加一层盐值拼接避免密码直接躺数据库里。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, real_name varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, email varchar(100) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role tinyint(4) DEFAULT 0, status tinyint(4) DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;宠物表pet宠物表是整个平台的流量入口字段设计直接决定列表页能不能好查。除了名字、品种、性别、年龄、毛色、绝育状态、疫苗状态这些基本属性一定要单独存cover_image作为封面图images字段存多图的JSON数组字符串查询列表时只取封面详情时再解析完整图集。status字段是这个项目最关键的字段取值范围0待领养、1已申请、2已领养、3已下架用数字而不是字符串检索效率高。publish_user_id用于追踪哪个用户发布的。领养申请表adoption_application领养申请表多对一关联宠物多对一关联用户。审核流转依靠status字段0待审核、1已通过、2已拒绝。有一个很关键的细节一个宠物同时只能有一个“待审核”状态的申请这个业务规则最好通过代码先查再写来保证而不是依赖数据库唯一索引——因为“待审核”是一个可变状态很难用静态索引约束。寻宠启事表lost_pet寻宠启事表包含宠物特征描述、丢失地点、丢失时间、联系方式、图片、status0寻找中、1已找到。这个模块要注意全文搜索问题。小项目里用LIKE %关键词%先顶着等数据多了再考虑上Elasticsearch或者MySQL全文索引。志愿活动表volunteer_activity和报名表activity_signup这两张表是关联关系一个活动可以被很多人报名一个人可以报多个活动。设计活动表的时候要存max_people和current_people两个字段报名的时候先查current_people max_people再插入报名记录同时用数据库事务包住两步操作防止“最后一个名额被两个人抢到”。signup表加一个创建时间字段活动审核时按报名先后顺序录取给用户展示时也能排序。公告表notice后台发布平台公告前台公告列表展示字段简单标题、内容、发布时间、状态。没有复杂的逻辑但前台首页推荐位经常要取最新一条公告所以创建时间字段记得建索引。2.2 项目分层结构与MyBatis的XML映射细节后端代码我用的是经典四层结构Controller层接收请求和参数校验Service层处理业务逻辑和事务Mapper层DAO层负责数据库读写Model层放实体对象。有人觉得这个结构老套但它最大的优点是职责边界清楚单人开发时写代码不用犹豫“这段逻辑到底放哪里”。MyBatis的核心配置要注意几个容易踩坑的点。第一mapUnderscoreToCamelCase必须设置为true否则数据库字段create_time无法自动映射到Java属性的createTime你只能写一堆resultMap手动映射纯属自找苦吃。第二实体类里凡是数据库不存在的字段——比如宠物列表查询时用的statusName中文状态名、搜索用的keyword——都要加TableField(exist false)注解如果引入MyBatis-Plus的话原生MyBatis不需要但要在XML的SQL里避免使用这些字段。一个带搜索的宠物列表查询SQL示例select idselectPetList parameterTypemap resultTypecom.demo.pet.Pet SELECT p.*, u.real_name AS publisherName FROM pet p LEFT JOIN user u ON p.publish_user_id u.id where if testkeyword ! null and keyword ! AND (p.name LIKE CONCAT(%, #{keyword}, %) OR p.breed LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND p.status #{status} /if if testtype ! null and type ! AND p.type #{type} /if /where if testorderBy ! null and orderBy ! ORDER BY ${orderBy} /if LIMIT #{offset}, #{pageSize} /select这个SQL有两个细节值得展开。第一个是LIKE拼接用了CONCAT(%, #{keyword}, %)而不是直接写%${keyword}%前者是占位符预编译能防SQL注入后者是字符串替换会被攻击。第二个是ORDER BY ${orderBy}直接接收外部参数用${}绕过预编译虽然理论上不优雅但排序字段没法用?占位符替代所以必须在后端白名单校验传入值只允许白名单里的字段名传进来否则就默认排序。2.3 登录鉴权JWT令牌如何在前后端之间流转这个项目的登录鉴权方案我选的是JWT而不是Session。和Session模式相比JWT是无状态的服务器不需要存会话记录适合前后端分离的场景也对后续横向扩展友好。用户登录成功后Controller层生成一个有效期为24小时的Token返回给前端前端每次请求都在Authorization头带上这个Token后端用拦截器统一校验。JWT的核心结构是三段式Header算法类型、Payload用户信息、过期时间、Signature签名。下面是一个标准的登录接口核心流程public String login(String username, String password) { User user userMapper.selectByUsername(username); if (user null || !user.getPassword().equals(MD5Util.encode(password))) { throw new BusinessException(用户名或密码错误); } if (user.getStatus() 1) { throw new BusinessException(账号已被禁用); } String token JwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); return token; }JwtUtil.createToken内部就是把用户ID和角色写进Payload再用密钥通过HMAC-SHA256算出签名。前端拿到Token后还需要解密拿到用户ID吗不需要后端从Token中解析每次请求拦截器都会把解析出的userId放到ThreadLocal里后续Service层的方法直接UserContext.getUserId()就能拿到当前用户这就是一个典型的上下文模式在Web项目里的应用。用ThreadLocal要注意请求结束时在拦截器的afterCompletion里调用remove()清理否则线程池复用时会出现用户串号。2.4 文件上传方案配置MinIO作为图片存储服务流浪动物救助的图片上传涉及很多宠物照片、寻宠启事照片、用户头像。开发阶段用本地文件存储加虚拟路径映射能快速跑通但部署上线后本地存储有两个硬伤服务器磁盘空间有限而且图片无法统一做备份。我项目中后期把存储切到了MinIO。MinIO是一个开源的、兼容Amazon S3协议的对象存储服务Java后端通过SDK就能上传和下载文件。安装很简单下载二进制包修改启动参数指定MINIO_ROOT_USER和MINIO_ROOT_PASSWORD即可日常开发在本地一跑就是一个私有化的图床服务。SpringBoot集成MinIO的核心配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB minio: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket-name: pet-images关键代码是上传逻辑。文件从MultipartFile变成MinIO对象需要一个putObject操作同时要设置对象的ContentType否则图片在浏览器里会被直接下载而不是预览。文件名的生成一定要用UUID或者时间戳加随机数千万不要用用户上传的原始文件名否则不同用户上传同一个名字的文件会互相覆盖。String fileName UUID.randomUUID().toString().replaceAll(-, ) . suffix; minioClient.putObject( PutObjectArgs.builder() .bucket(pet-images) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() );MinIO中Bucket的权限默认是私有的也就是说直接通过URL无法访问文件。有两种处理方式要么把Bucket的Policy设置为public这是最简单粗暴的开发阶段就用它要么后端写一个临时访问链接接口通过MinIO的presignedGetObject生成一个带有效期比如7天的签名URL。生产环境建议用后者防止图片被外部程序批量抓取。2.5 分页查询与条件搜索的通用实现套路后台管理列表和前台宠物列表都需要分页和条件搜索。我用了一种在单体项目里非常通用且好理解的实现方式前端传pageNum、pageSize、keyword等参数后端手写一个PageResult包装类配合PageHelper插件实现分页。PageHelper用法的坑网上讨论很多核心就一条必须在查询前调用PageHelper.startPage(pageNum, pageSize)而且紧接着的第一条SQL查询就是你需要分页的查询中间不能插入任何其他数据库操作。MyBatis-Plus的IPage是面向对象的分页原生MyBatis配合PageHelper则是面向SQL的分页如果你用XML手写了一个复杂的LEFT JOIN多表查询PageHelper是能自动套上COUNT语句的这也是选它最省事的原因。分页结果前端一般要拿到total总记录数来算总页数后端返回的结构统一是{ code: 200, message: 操作成功, data: { list: [], total: 100, pageNum: 1, pageSize: 10 } }我强烈建议把一个项目里所有接口的返回值统一成ResultT格式Controller里永远返回这个对象不要因为图方便而直接返回裸数据。统一响应格式能让你在前端写Axios拦截器时极其舒服错误码和业务数据的解析逻辑只需要写一次。3. 前端架构Vue页面组织与组件复用3.1 Vue工程搭建、路由设计和前端目录结构前端用Vue 2.6配合Vue CLI 4.x脚手架创建工程。Vue CLI创建项目的命令很简单关键是我推荐在创建时勾选Router和Vuex这两个依赖在项目里都会用到。创建完成后一个规范的前端目录结构是下面这样src/ ├── api/ // 所有接口调用封装 │ ├── pet.js │ ├── user.js │ ├── adoption.js │ └── request.js // axios实例封装 ├── assets/ // 静态资源 ├── components/ // 公共组件图片轮播、分页组件、上传组件 ├── router/ // 路由配置 │ └── index.js ├── store/ // Vuex状态管理用户信息、Token ├── views/ // 页面组件 │ ├── admin/ // 后台管理端页面 │ ├── system/ // 系统管理页面 │ └── home/ // 前台页面 ├── App.vue └── main.js路由设计是整个前端最关键的一环。因为系统有前台和后台两套界面我把路由拆成了两组home布局下的前台路由以及admin布局下的后台路由。两者的区别不只是路径前缀不同更重要的是一套权限控制机制后台路由必须在登录状态下才能访问管理员角色才能进入。Vue Router的守卫函数beforeEach是实现权限控制的标准姿势伪代码逻辑如下router.beforeEach((to, from, next) { const token store.state.token; if (to.path.startsWith(/admin)) { if (!token) { next(/login); } else if (store.state.user.role ! 1) { next(/403); // 权限不足 } else { next(); } } else { next(); } });注意一个细节路由守卫中检查token存在只是确认用户登录过不能确认用户是否还有效。这是因为JWT的过期校验在后端拦截器进行前端需要的只是一个粗略的跳转拦截。Token过期后前端调用接口时后端会返回401状态码这时候在Axios响应拦截器里统一清除本地Token并跳转登录页这才是完整的一套闭环。3.2 Axios封装拦截器才是前后端交互的中枢前端的接口请求我全部封装在一个request.js文件里。这个文件里导出一个配置好的Axios实例请求拦截器统一把Token加到Header上响应拦截器统一处理状态码。import axios from axios; import router from /router; import store from /store; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use( config { const token store.state.token; if (token) { config.headers[Authorization] token; } return config; }, error Promise.reject(error) ); request.interceptors.response.use( response { const res response.data; if (res.code 401) { store.commit(clearAuth); router.push(/login); return Promise.reject(new Error(登录过期)); } if (res.code ! 200) { // 统一弹出错误提示 return Promise.reject(new Error(res.message)); } return res.data; // 直接把业务数据返回给页面 }, error Promise.reject(error) );这个封装的精华在响应拦截器的return res.data。如果没有这一步页面上每次拿数据都要写response.data.data.list这种三层嵌套的对象页面代码会非常啰嗦。有了它页面里调接口拿到直接就是业务数据代码干干净净。另外要理解baseURL: /api是什么意思。开发环境下Vue CLI的devServer会代理转发/api开头的请求到后端服务器生产环境下Nginx也要配置一个location /api的转发规则。这个过程就是前后端分离项目的“跨域解决方案”不需要在后端开启CorsFilter——当然开发阶段图省事开启一下也可以上线时最好交给网关代理。3.3 前台展示页实现要点轮播图、宠物卡片、分页列表前台页面是用户最容易感知的部分。我大概用了十几个Vue组件来组装整个前台。宠物列表页用一个PetCard组件展示单只宠物卡片包括封面图、名字、品种、绝育状态、状态标签可领养/已申请/已领养父组件用v-for循环数据源渲染卡片列表然后配一个通用分页组件。宠物详情页的图片区域我用了Vue版的Swiper轮播组件。宠物详情页还有一个特殊逻辑如果在详情页能看到“发布者联系方式”只有登录用户才能看到未登录状态显示“请登录后查看联系方式”。这种细粒度的控制逻辑在前端模板里直接判断token是否存在就行真正的数据敏感操作——比如提交领养申请后端必须再校验登录状态和申请权限前端隐藏只是体验层面的优化不能作为安全机制使用。后台管理页面用了Element UI的el-table、el-form、el-dialog、el-pagination这套组合配置好数据源和事件方法一个标准的后台CRUD页面大概150行代码就能写出来。我写后台页面的套路是每个功能模块一个目录目录下放index.vue列表页和form.vue编辑弹窗两个文件配合使用代码结构统一后续维护时查看某个功能的实现非常省时间。3.4 前端本地开发跨域代理配置与Mock数据策略前后端联调是开发中不可避免的繁琐环节。我在开发初期习惯用Mock数据先把前端界面写好跑通等后端接口就绪后再切换真实接口。这个方法能大大压缩纯联调等待时间。实现方式不复杂Axios封装里判断一个USE_MOCK环境变量为true时直接走本地的mock/*.js文件返回假数据为false时走真实API。等后端接口调试完毕把这个开关切回来就行。真正联调的时候Vue CLI的vue.config.js里需要配置devServer代理把请求转发到后端SpringBoot的8080端口避免开发期一直受跨域问题折磨module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };这里要注意一个反向代理的常见坑后端Controller的RequestMapping路径如果写成了/api/pet/list那么代理的pathRewrite会把/api前缀剥掉再转发到后端如果后端路径本身又包含/api就会404。最好的习惯是前端约定所有请求都带/api前缀后端Controller统一用/api开头代理上去掉前缀还是保留要看实际调试情况前后端保持同一种约定就不会出问题。4. 完整实操流程从初始化到跑通全系统4.1 环境准备与项目初始化清单完整跑通一个项目环境准备是最琐碎也最容易出问题的一步。我把自己实测过的一套环境版本和配置列在下面直接对照着装就行。JDK 1.8 配置JAVA_HOME环境变量别用JDK 21跑SpringBoot 2.7会直接启动失败Maven 3.6 国内建议配阿里云镜像源否则下载依赖能等到怀疑人生MySQL 5.7 安装时选UTF-8字符集root密码记好Node.js 14.16 npm安装依赖必需IDEA 2022后端开发VSCode或WebStorm前端开发可以用同一个IDEA装Vue插件也行Navicat或MySQL Workbench数据库可视化管理后端工程我用IDEA自带Spring Initializr创建选择Spring Boot 2.7.18依赖勾选Spring Web、MyBatis框架和MySQL Driver。注意原生MyBatis在Spring Boot中是mybatis-spring-boot-starter如果你用的版本是2.x还要确认JDK版本和SpringBoot版本相匹配。前端的初始化命令是npm install -g vue/cli vue create pet-frontend创建后安装Element UI和Axiosnpm install element-ui npm install axios到这里前后端的空工程框架就搭好了。4.2 核心接口开发全流程以领养申请为例领养申请是整个系统里业务逻辑最复杂的接口用它来展开完整的后端开发流程最有代表性。领养申请的前提是用户已登录宠物存在且状态为0待领养该用户没有对这个宠物处于“待审核”状态的申请。接口定义POST /api/adoption/apply 请求体: { petId: 1, reason: 我想要领养它 } 响应: 申请成功Service层核心代码如下Transactional public void applyAdoption(AdoptionApplication application, Integer userId) { Pet pet petMapper.selectById(application.getPetId()); if (pet null || !pet.getStatus().equals(0)) { throw new BusinessException(该宠物不可领养); } int applyCount adoptionMapper.selectCountByPetIdAndStatus(pet.getId(), 0); if (applyCount 0) { throw new BusinessException(该宠物已被其他用户申请请等待审核结果); } application.setUserId(userId); application.setStatus(0); adoptionMapper.insert(application); // 关键点更新宠物状态为“已申请” pet.setStatus(1); petMapper.updateById(pet); }这段代码有三个关键设计值得反复琢磨第一个是Transactional注解。更新宠物状态和插入申请记录这两个数据库操作必须同时成功或同时失败。如果不加事务万一申请记录插入成功但宠物状态没有更新成功就会出现一个宠物被两个用户同时“申请中”的脏数据。事务这个概念可以理解为“把所有操作打包成一个整体要么全部完成要么全部回滚”。第二个是查询和插入不是原子操作。高并发场景下两个用户同时发起领养申请都查到了“当前没有申请”然后都插入了申请记录。这种情况在真实环境存在的虽然概率低但最好在adoption_application表加一个(pet_id, status)的联合唯一索引这样第二个插入就直接报数据库唯一约束异常代码里捕获异常后提示“该宠物已被申请”。数据库约束兜底永远比自己写判断逻辑更稳妥。第三个是宠物状态从0变成1之后前端宠物列表页要同步刷新。这里我建议的是列表接口查询时直接过滤掉状态3以下架的宠物但保留“已申请”状态的宠物在列表上显示“已被申请等待审核结果”的标签。业务细节越贴近真实场景这个项目的完成度看起来就越高。4.3 前后端联调与整体跑通的检查清单前后端都开发完成后在本地做一次完整的功能走查是必须的不要直接打包部署。我总结了一份极简的功能走查清单照着一项项打勾能覆盖绝大多数问题功能模块测试操作预期结果用户登录正确密码和错误密码各测一次正确密码登录成功错误提示提示宠物列表查看首页宠物列表已下架的宠物不出现已申请的显示“申请中”标签领养申请对同一宠物重复提交申请第二次提交被拒绝提示已被申请领养审核管理员审批通过一个申请宠物状态变为已领养申请单显示“已通过”活动报名报名满员的活动提示名额已满插入失败文件上传上传一张大图10MB请求被拦截提示文件过大权限控制普通用户访问后台路由被路由守卫拦截跳转登录页Token过期人为修改本地Token值再访问后端返回401前端清除登录态并跳转登录页联调成功后最后一步是前后端构建。前端执行npm run build生成dist目录里面是纯静态文件可以交给Nginx托管。如果你希望部署成一个SpringBoot单体应用也就是把前端静态文件直接塞进SpringBoot一起启动也是可行的。在后端工程的src/main/resources目录下创建一个static文件夹把dist目录里的index.html和static子目录整体放进去SpringBoot自动就会把它作为静态资源服务起来。需要额外注意的是Vue Router用history模式时刷新/admin/pet/list页面会导致404因为后端没有对应的路由处理解决方式是后端加一个WebMvcConfigurer把前端路由的URL全部转发到index.html或者最简单的方法是把Vue Router换成hash模式URL中带#号但不会404。我实际经验是小项目直接用hash模式最省事不留隐患。5. 常见问题与排查技巧实录5.1 数据库连接类问题SSL连接错误与时区报错很多初学者第一步就在连数据库时被卡住报错信息五花八门最常见的是这样两行Establishing SSL connection without servers identity verification is not recommended java.sql.SQLException: The server time zone value EDT is unrecognized or represents more than one time zone根因是MySQL 5.7默认开启了SSL验证而本地开发连接的JDBC没有提供证书同时MySQL服务器的时区配置和中国本地时区不一致。解决方式是在连接字符串上明确指定不用SSL、时区用东八区jdbc:mysql://localhost:3306/pet_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4allowPublicKeyRetrievaltrueallowPublicKeyRetrievaltrue这个参数很多人不知道它解决的是MySQL 8.0使用caching_sha2_password插件时的公钥检索问题MySQL 5.7不牵扯。但如果你用了MySQL 8.0这个参数必须加上否则连接会直接失败。我一般直接建议初学者锁死MySQL 5.7就是为了一次性能把这些坑全避开。5.2 MyBatis映射与缓存常见报错和场景实测MyBatis最常见的报错之一是Invalid bound statement (not found): com.xxx.mapper.PetMapper.selectPetList出现这个错99%的原因是Mapper接口和XML文件的对应关系没有被Spring扫描到。检查三个位置MapperScan注解是否配置了正确的包路径application.properties中mybatis.mapper-locations是否指向了classpath*:mapper/*.xmlXML文件里的namespace是否和Mapper接口的完整类名完全一致。这三个位置任何一个错了都会报这个错。我自己实际开发中会优先打开编译后的target/classes目录看XML文件到底有没有被打包进去这是排查这个报错最直接的方法。MyBatis的缓存机制也是一个容易困惑的点。一级缓存是SqlSession级别的默认开启同一个SqlSession内执行同一条SQL两次第二次直接命中缓存不会查数据库二级缓存默认关闭需要配置cache/才能开启。我实际开发中强烈建议小项目直接关闭二级缓存开启反而容易遇到缓存脏读的问题。因为一旦一个表的数据被别人改了比如通过别的SqlSession你本地SqlSession的缓存仍然是旧数据这会在联调时造成非常隐蔽的Bug数据明明改了但页面上不刷新。排查时在MyBatis的配置里把日志级别调到DEBUG就能看到SQL是否真的执行了。5.3 前端开发高频问题npm依赖安装异常和跨域请求失败npm install在国内环境跑起来经常卡住不动或者报各种ECONNRESET错误。替代方案很简单配置淘宝npm镜像源npm config set registry https://registry.npmmirror.com然后重新执行npm install基本就顺畅了。这个镜像源配置一次全局生效后续所有npm项目都走国内镜像实测速度提升非常明显。前端页面在开发环境调后端接口时如果没配置代理Network面板的请求状态是CORS error。有一个快速验证CORS问题的方法按F12打开Network面板如果请求失败但Response Header里有Access-Control-Allow-Origin字段那代理配置大概率有问题仔细检查vue.config.js的pathRewrite。如果Response Header里压根没有CORS相关字段说明请求根本没走到后端是代理层的问题。用这个思路十次能解决八次跨域问题。Vue打包放进SpringBoot还有一种特殊场景部署后刷新页面404但首页能正常访问。这个就是我前面提到的history模式问题最简单的方法是把Vue Router的mode改成hashconst router new VueRouter({ mode: hash, routes });改完后URL变成http://域名/#/admin/pet/list刷新不会触发后端路由匹配一个配置就解决了问题代价是URL里多个#号对内部系统来说完全无伤大雅。5.4 后端部署的几个隐藏细节端口占用、时区配置、内存限制后端部署到服务器有四个坑我几乎每次都会遇到。第一个是端口占用SpringBoot默认8080如果服务器上已经有一个服务占了8080端口应用启动直接报Port already in use。解法很简单换端口或者杀掉占用进程但更优雅的做法是在启动脚本里通过--server.port8081指定端口这样配置文件不用动。第二个是服务器时区和MySQL时区不一致导致的时间错乱问题。这个是最隐蔽的坑数据库存的时间是2024-01-01 00:00:00代码查出来变成2024-01-01 08:00:00整整快8个小时。原因从前面第5.1节就能找到——服务器没有配置时区环境。一次性解法是在MySQL连接串上继续指定serverTimezoneAsia/Shanghai同时把服务器的时间和时区设置好这样前后端、数据库三方时间就统一了。第三个是内存限制。一个SpringBoot应用默认JVM堆内存是物理内存的四分之一小内存服务器比如1G或2G的云主机上很容易内存溢出。部署时一定记得显式设置JVM参数java -Xms256m -Xmx512m -jar pet-system.jar这样堆内存控制在512MB以内1G内存的服务器足够跑起一套SpringBoot加MySQL了。第四个是LocalDateTime返回给前端时被序列化成一长串数字而不是2024-01-01 12:00:00的格式。这是因为Jackson默认把LocalDateTime序列化为时间戳。解决方式是统一加上JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)或在配置文件里配置Jackson的日期格式全局生效spring.jackson.date-formatyyyy-MM-dd HH:mm:ss spring.jackson.time-zoneGMT8这个坑非常高频几乎每个前后端分离项目都会遇到一次。6. 避坑总结与拓展方向最后聊点实际的。这个项目从我搭框架到所有功能跑通大概用了两周时间经验全部踩完一遍之后有几个真实体会想分享给正在做同类项目的人。第一个体会是不要过早优化也不要过度设计。我的第一版数据库设计实际上是7张表后来砍到6张还有一张冗余表被拆成了字段。做项目的正确姿势是先把核心流程跑通——用户登录、宠物上架、领养申请、后台审核再考虑加功能。哪怕功能做得粗糙一点一条完整链路跑通带来的信心比很多半成品功能有用得多。第二个体会是关于源码的可读性。自己写自己调试的时候命名怎么随意都能看懂但一个项目隔一周再打开脑子里对代码的记忆几乎清零这时候就会感谢当初给每个类、每个方法都起了精准的名字。我这个项目的类名规则是Controller类用模块名加Controller后缀Service接口用模块名加Service实现类加ServiceImplMapper接口用模块名加Mapper一眼就能看出类在架构中的位置。代码里该加注释的地方加注释尤其对于事务、状态流转这种业务逻辑写一行注释说明“这个操作为什么这样判断”对后来的自己帮助极其大。第三个体会是前端框架版本别乱升级、后端依赖别乱折腾。项目用的是一套经过大量验证的版本组合一旦升级其中一个可能连带要升级其他模块然后就会陷入依赖冲突的泥潭。这套组合我用了很多次从来没出过大问题谁换谁知道。至于后续可以扩展的方向按实际价值排序大概是这三个一是接入真正的在线支付支付宝沙箱或者微信支付API把捐款模块从模拟变成真能收钱二是引入Redis做热点数据的缓存宠物列表、公告信息减轻数据库压力三是在前端加入ECharts可视化大屏做救助站的数据统计展示领养率、活跃用户数、活动参与人次等视觉效果好完成度感知也高。这三个方向无论哪个都是在现有代码结构上增量开发不会伤筋动骨。最后再分享一个小技巧整套项目的源码我建议每个人在自己电脑上从零手敲一遍不要直接复制粘贴运行。手敲的过程中遇到的每一个报错都是在帮你在面试时说出“这个问题我遇到过原因是什么、解决方案是什么”。这种真实经验远比背面试题答案更有说服力也更能体现一个Java开发者的基本素养。
返回列表