ARTICLE DETAIL

资讯详情

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

前后端分离流浪猫狗救助网站系统实战:从0到1完整记录

前后端分离流浪猫狗救助网站系统实战:从0到1完整记录 前后端分离流浪猫狗救助救援网站系统从0到1的完整实战记录老实说这种公益向的项目特别适合拿来练手全栈。流浪猫狗救助站这类系统既要有普通CMS的内容展示能力又涉及救助申请、物资捐赠、领养记录等真实业务流数据模型比“学生管理系统”复杂得多但又不至于像电商那样重。我去年帮一个本地动物保护组织做过一套前后端分离、源码可跑、部署可复现积攒了不少一手经验今天就把这套完整方案拆开揉碎讲清楚。适合已经在学SpringBoot和Vue、想找真实项目练手的同学也适合刚接手这类项目的开发者直接“抄作业”。1. 整体设计与技术选型1.1 为什么坚决选择前后端分离动物救助网站有个特点页面样式经常要改。复活节要放领养主题活动年底要做年度报告专题页平常还要不断优化“待领养列表”的展示形式。如果是传统JSP/Thymeleaf模板方案每一次前端改动都要动后端代码发布节奏直接绑死。我见过好几个用JSP做的老系统改个按钮颜色都要后端发版特别痛苦。前后端分离之后前端Vue工程负责所有页面渲染和交互后端SpringBoot只对外暴露标准JSON接口。两边独立开发、独立部署前端要改版直接动Nginx静态资源就行后端接口保持稳定即可。另一个隐性好处是接口文档天然沉淀下来每个接口对应一个前端请求函数对接联调时两边各看各的不用反复翻代码确认字段名。1.2 技术栈的取舍逻辑这套系统用了比较经典的四件套SpringBoot Vue MyBatis MySQL。有人可能会问为什么不选Spring Cloud微服务不选MyBatis-Plus不选Vue3TypeScript原因很简单——这个项目的规模和维护场景决定了技术栈不能过度设计。救助站系统的并发量、数据量都不大单体应用完全扛得住维护者可能是下一届志愿者技术栈越主流越好招人接手。SpringBoot 2.7.x配MyBatis是面试和工作中最常见的组合Vue2的生态成熟稳定Element UI组件库拿来即用学习成本远低于组合复杂的Vue3TS。不过有两个小升级是我强烈建议做的MyBatis框架本身够用但项目里有大量单表CRUD写XML繁琐容易出错后续可以引入MyBatis-Plus提升效率MySQL如果后续有全文搜索需求建议直接走Elasticsearch或MySQL全文索引而不是硬写LIKE模糊查。1.3 能跑起来的核心功能矩阵这套系统要真正用起来至少要覆盖以下几个模块我也按业务优先级排了序首页内容展示轮播图、最新公示、待领养猫咪狗狗列表宠物档案完整的流浪动物资料卡含照片、性格描述、健康状况、救助故事救助申请流程用户提交领养/救助申请管理员后台审核流转物资捐赠记录在线申请捐赠、物资入库出库管理志愿者管理志愿者注册、活动报名、服务时长登记后台管理用户管理、内容发布、数据统计、审核处理表结构设计上围绕这几块业务建了十张左右的表用户表、角色表、宠物信息表、救助申请单表、捐赠记录表、志愿者活动表、文章内容表、轮播图表、公告表。这套表结构拆开了就是标准的“用户-业务-内容”三层模型几乎可以直接平移去做其他公益系统。2. 环境准备与脚手架搭建2.1 拿什么版本的软件才能不踩坑环境问题真的是第一只拦路虎。我在搭建这套系统时走了不少弯路先把一套稳定组合放出来组件推荐版本避坑建议JDK1.8 或 11别一上来就装JDK17老项目兼容性最好的是8/11SpringBoot2.7.18别用3.x很多教程和起步依赖都按2.x写MyBatis3.5.x配SpringBoot2.x是绝配MySQL5.7.x 或 8.0.x5.7.44较稳8.0需要确认驱动名和时区设置Vue2.6.x Vue CLI 4/5Vue3语法差异大教程对照容易绕晕Element UI2.15.x配Vue2的主力UI库Node.js14.x 或 16.x太高版本编译node-sass容易失败Nginx1.2x静态托管反向代理比直接用Tomcat跑前端更灵活先说后端。SpringBoot版本千万别跟风追新网上教程、依赖仓库、mybatis-spring-boot-starter的适配版本几乎都是围绕2.x的。我用2.7.18跑通全部功能一次版本冲突都没遇到。你要是装完发现注入不了Mapper、包扫描失败先看看是不是SpringBoot 3.x的锅——3.x改用了Jakarta命名规范很多老配置直接失效。再说前端。Vue的版本问题真的是老生常谈。用Vue CLI脚手架创建的默认工程是Vue3不对——Vue CLI 5默认创建的是Vue3选择Default preset时实际是3.x但很多老教程讲的Vue2写法、main.js里new Vue({el: #app})那种在Vue3项目里直接不能跑。建议创建的时候选Manually select features再选Vue 2.x版本进程会少很多鬼打墙的问题。2.2 数据库初始化注意的细节MySQL连不上的头号原因永远是驱动和时区。用MySQL 8.0的时候驱动要写成com.mysql.cj.jdbc.Driver旧版的com.mysql.jdbc.Driver已经被移除。连接串必须加上时区参数serverTimezoneAsia/Shanghai否则启动时必报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。建库的时候我建议统一用UTF8MB4字符集因为救助站的内容是中文为主万一以后要存emoji比如宠物名字里带utf8mb3直接存不了。推荐的建库语句CREATE DATABASE rescue_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;2.3 Maven依赖的一处关键坑后端pom里最核心的依赖就这几样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies特别注意mysql-connector-j这个驱动坐标。SpringBoot 2.7.x的依赖管理里有mysql:mysql-connector-java和com.mysql:mysql-connector-j两种写法前者是老坐标后者是8.0.31后的新坐标我实测下来新坐标更可靠。如果你强行指定了旧版本的驱动包反而可能因为类路径冲突导致连不上MySQL 8。3. 后端核心模块设计与实现3.1 项目包结构先定边界再写代码后端代码不能全堆在一个包下。我的分包策略是自外而内逐层收拢com.rescue.system ├── controller # 接口控制层只负责参数接收和返回 ├── service # 业务层事务边界都在这一层 ├── dao # MyBatis Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端交互数据对象 ├── vo # 视图对象给前端展示用 ├── config # 配置类跨域、拦截器、MyBatis配置 ├── common # 公共类统一返回值、异常处理、工具类 └── utils # 工具类这个结构的核心思想是分层清晰、依赖单向。Controller不直接操作DAOService不直接暴露Entity给前端数据在层与层之间转换的时候有明确的“户口”。比如宠物信息表里存着一个vaccinated字段取值范围是0/1但前端希望拿到“已完成/未完成”的中文状态——这个转换放在VO层做而不是在SQL里折腾。3.2 统一返回体前后端协作的地基前后端分离项目最怕的就是接口返回格式不统一。今天这个接口返回{code: 200, data: {...}}明天那个接口直接返回{success: true, result: {...}}前端写公共请求封装的时候会疯掉。我从一开始就定了统一返回类Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }所有Controller方法都返回这个Result前端axios拦截器里统一判断code 200再放行否则直接弹message。实际开发中这套体系统一之后至少避免了20%的联调返工。另外务必要做的还有全局异常处理器。用RestControllerAdvice拦截业务异常、参数校验异常、SQL异常转换成统一返回体。这个模块写起来才不到一百行但作用巨大——不然后端SQL报错直接返回英文堆栈前端一脸懵。3.3 爱心宠物档案MyBatis联表查询实战宠物列表页是个典型的复杂查询场景需要展示宠物基本信息、救助经历、当前状态待领养/已领养/救助中、所在救助站名称、护理人名字还要支持按品种、年龄、状态筛选。这些数据分散在pet、rescue_station、user三张表里用MyBatis的联表查询一次拉回来不要在Service里多次查询性能差一个数量级。核心的XML mapper片段select idselectPetDetail resultTypecom.rescue.system.dto.PetDetailDTO SELECT p.id, p.name, p.species, p.breed, p.age_months, p.gender, p.status, p.description, p.photos, s.name AS station_name, u.nickname AS caregiver_name FROM pet p LEFT JOIN rescue_station s ON p.station_id s.id LEFT JOIN user u ON p.caregiver_id u.id where if testspecies ! null and species ! AND p.species #{species} /if if teststatus ! null and status ! AND p.status #{status} /if /where ORDER BY p.create_time DESC /select这里有个细节LEFT JOIN在数据正常的时候和INNER JOIN没区别但一旦救助站或护理人的记录被删了比如志愿者离职清档还能看到宠物基础信息。公益系统里的历史数据不能因为人员变动就彻底不可见所以一律用LEFT JOIN。MyBatis管理参数名的时候Service层调用必须加上Param(species)这类注解否则框架反射拿不到真实参数名。如果你的Mapper接口方法参数用了多个条件这一点必踩直接把param1、param2错乱到怀疑人生。3.4 救助申请流程的状态机设计救助申请是所有业务里流转逻辑最复杂的用户提交一份申请后要经过站内初审、线下核实、结果反馈三个节点。这个流程不能随便用if-else堆建议用一个简单的状态枚举public enum ApplyStatus { PENDING(0, 待审核), REVIEWING(1, 审核中), APPROVED(2, 已通过), REJECTED(3, 已拒绝), CANCELLED(4, 已取消); private final Integer code; private final String desc; }状态流转的核心规则就四个待审核能转审核中或拒绝审核中能转通过或拒绝通过/拒绝是终态申请在任何非终态下都可以由用户主动取消。这个判断放在Service层做一个独立方法不要散落在各处。我在实际代码里就用了一个简单的Mapstatus, ListnextStatus来做合法性校验而不是写一大堆if-else。事务也得管好用户提交申请要同时更新申请表和宠物状态如果宠物在提交的瞬间已经被其他用户申请了这里就不能再成功。给Service方法加上Transactional(rollbackFor Exception.class)排查BUG的难度会直线下降。3.5 权限控制Spring Security还是拦截器初期系统只有普通用户和站长管理员两种角色上Spring Security有点重我选择了SpringBoot拦截器方案。写一个AuthInterceptor在preHandle里校验请求头里的token从Redis或数据库查询出用户角色再检查当前请求路径是否在白名单之外public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、注册、获取轮播图等接口 if (isPublicPath(request.getRequestURI())) { return true; } String token request.getHeader(Authorization); // 解析token校验登录状态... } }跨域配置也要跟上。前后端分离开发时前端跑在8080端口后端在8081不配置CORS的话浏览器直接拦截所有请求。我的建议是在后端的WebMvcConfigurer里统一配置而不是在后端每个Controller上加CrossOrigin注解——后者在接口多的时候会漏加而且运维期也很难统一改。4. 前端路由与核心功能实现4.1 Vue工程目录结构规划前端不能见一个页面就建一个.vue后面绝对会变“屎山”。我的目录规划src ├── api # 所有接口请求封装按模块分文件 │ ├── pet.js │ ├── apply.js │ ├── user.js │ └── admin.js ├── assets # 静态资源 ├── components # 可复用组件 │ ├── common/ # 通用组件图片上传、分页、富文本 │ └── module/ # 业务组件宠物卡片、申请表格 ├── router # 路由配置 ├── store # Vuex状态管理 ├── views # 页面组件 │ ├── home/ │ ├── pet/ │ ├── apply/ │ ├── user/ │ └── admin/ ├── utils # 工具函数request.js封装的axios实例 ├── App.vue └── main.jsapi/目录是整个前端数据的“门面”所有接口函数集中放这里页面组件不直接发axios请求。比如api/pet.js里import request from /utils/request export function getPetList(params) { return request({ url: /api/pet/list, method: get, params }) } export function submitApply(data) { return request({ url: /api/apply/submit, method: post, data }) }这样的好处是接口一多你打开pet.js就能看到所有宠物相关的接口而不是翻遍每个组件去搜axios。后续后端接口路径变了只需要改一个文件。4.2 axios请求封装与token管理utils/request.js里的axios实例是前端的基建工程我贴一个核心版本import axios from axios import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }, error Promise.reject(error)) service.interceptors.response.use(response { const res response.data if (res.code ! 200) { if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message || 请求失败)) } return res }, error { return Promise.reject(error) }) export default service这里有一个小坑后端统一返回体里code字段用的是数字类型如果你用res.code ! 200判断而后端某次不小心返回了字符串200判断会直接失败。排查过多起这种低级错误后我在前端统一用Number(res.code) ! 200后端也规范死返回数字类型。token管理上的建议是写进localStorage而不是sessionStorage。用户在浏览器关掉再打开时sessionStorage会清空而localStorage不会配合后端token有效期逻辑用户体验更好。4.3 宠物卡片列表的分页实现宠物列表页的分页直接套Element UI的el-pagination但要注意前端拿到的是分页参数不是一次性把所有数据都加载出来。后端接口设计为// 请求参数 { page: 1, pageSize: 12, species: cat, status: 0 } // 响应数据 { code: 200, data: { total: 57, list: [ { ... }, { ... } ] } }前端el-pagination的current-change事件绑定时要特别小心不要等接口返回后才更新当前页数据分页的时候可以先把loading置为true数据返回后再关闭不然页面会闪一下。列表改动的时候Vue的渲染是响应式的但photos字段如果存的是JSON字符串因为我后端统一存[url1,url2]前端展示之前必须要做一次JSON.parse这个转换可以在计算属性里统一做别在每个卡片组件里做重复功。4.4 后台管理界面的开发节奏管理员后台我用Vue Router的嵌套路由加动态菜单实现在/layout主框架组件里放一个侧边栏和一个内容区router-view各业务模块页面都挂在layout的子路由下。这样做的好处是后台所有页面共享一套布局不需要重复写侧边栏和顶部导航。后台的核心是表单和表格的配合宠物管理页左边是筛选条件中间是数据表格右边或弹窗里是录入表单。Element UI的el-form配上rules校验规则加上validate-on-rule-change属性能省下大量手工判断逻辑。区块刷新的问题也值得提一句后台列表页经常遇到“数据改完了但列表没刷新”的情况根因是Vue响应式劫持不到某些写法下的数组变化。解决方案是确保所有列表数据更新都走this.$set(this.list, index, newData)或者干脆在接口返回后重新拉取一页数据后者更简单也更稳。4.5 前端环境变量与代理配置开发阶段前后端端口不同跨域问题是没法绕开的。除了后端配CORS更推荐在前端Vue CLI的vue.config.js里配devServer代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样前端代码里所有请求都写成相对路径/api/pet/list代理自动转发到后端8081端口。发布到生产环境后Nginx再配一次同样的反向代理规则。整个过程中前端代码的请求路径完全不变不用区分环境改接口地址。5. 部署上线与常见问题排查5.1 前后端各自独立部署的最佳姿势这套系统的部署方案我推荐前端静态资源挂Nginx后端JAR包独立跑。后端打包前记得在application.yml里把数据库连接配置改成生产环境值然后执行mvn clean package -Dmaven.test.skiptrue打包完成后会生成类似rescue-system-1.0.0.jar的文件直接java -jar rescue-system-1.0.0.jar --spring.profiles.activeprod前端打包命令是npm run build生成dist/目录。把这个目录整个拷贝到服务器Nginx配置如下server { listen 80; server_name your-domain.com; location / { root /var/www/html/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html这一行的作用是把未知路由都重写到首页。如果没有这行用户访问/pet/12详情页时刷新一下浏览器直接404前端路由喂不了的就要后端兜底。这个配置可以说是前后端分离部署的“保命三连”。端口和防火墙也要注意。后端8081端口如果被防火墙挡了前端代理过去必然失败。生产环境里我们通常只对外开放80把8081限制在内部访问仅仅通过Nginx暴露/api路径出去安全面上少了很多无谓的暴露面。5.2 经典问题实录SpringBoot启动失败我搭建调试过程中出现过一次特别有代表性的启动报错Error creating bean with name petMapper ... Invalid bound statement (not found): com.rescue.system.dao.PetMapper.selectPetDetail这个错误的意思是Mapper接口找到了但XML文件没绑定上。排查步骤是确认PetMapper.java接口和PetMapper.xml文件在同一个包路径下或者配置了mapper-locations。确认XML文件的namespace属性值和接口全限定名完全一致。确认application.yml里的mybatis.mapper-locations配置是classpath:mapper/*.xml的正确写法。我当时卡了半小时最后发现是自己粗心把XML放到了src/main/java目录下而不是src/main/resources目录下。Maven默认不会把java目录下的XML打包进classpath所以运行时一直找不到。解决方法是放进resources目录或pom里额外配置资源目录。5.3 经典问题实录前端页面白屏与路由404Vue项目部署后白屏十有八九是资源路径问题。默认的base路径是/但如果你把前端资源部署到服务器的子路径下比如/rescue/资源全部404自然白屏。两个改法把vue.config.js里publicPath改为/对应实际部署位置。或者路由改用createWebHashHistory()URL带#/的形式不依赖服务器路径。后端接口404的问题通常是Nginx的proxy_pass配置路径对不上。如果前端请求的是/api/pet/list而Nginx配置里location /api/转发到了http://127.0.0.1:8081且没有保留路径导致后端收到的请求变成了/pet/listSpringBoot的RequestMapping(/api/pet/list)就匹配不上。解决办法是在proxy_pass末尾加上/保底思路location /api/ { proxy_pass http://127.0.0.1:8081/; }5.4 经典问题实录MySQL插入中文乱码中文乱码基本分两类情况。一是数据库和数据表本身的字符集没设置成utf8mb4需要在建库、建表语句里显式声明二是应用连接串里没加characterEncodingutf8jdbc:mysql://localhost:3306/rescue_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai还有一个隐蔽的场景MySQL 8.0默认字符集其实已经是utf8mb4但如果你从5.7导出的SQL脚本建表语句里可能带旧字符集导入新库时就会冲突。老老实实在导入前把SQL文件里的CHARSET全部替换成utf8mb4。5.5 上线前性能和安全自检清单按我这套系统的实际情况上线前建议至少过一遍这个清单连接池配置SpringBoot默认的HikariCP连接池建议调整maximum-pool-size小项目10-20足够别默认拉到10就开跑。SQL注入防护MyBatis的#{}参数替换是预编译的天然防注入但${}就要非常克制只用在表名/排序字段等少数场景。文件上传救助站有宠物照片上传功能务必校验文件类型和大小图片保存到独立静态目录而不是直接塞数据库BLOB。密码存贮用BCrypt算法存哈希不要明文裸奔。Spring Security里自带的BCryptPasswordEncoder可以直接用。接口限流可以用简单的拦截器对提交类接口做IP维度的频率限制防止恶意刷申请单。日志输出生产环境日志级别调到INFO把SQL执行日志单独打印到sql.log文件排查问题的时候不干扰业务日志。6. 这套系统的扩展空间与我的实操体感项目能跑通只是第一步公益系统的价值在于持续运营和迭代。我做完这个项目后的体会是还有几个方向可以花时间去扩展一个是微信小程序端。救助站的大量用户是习惯用手机刷内容的年轻人微信小程序入口比网页低了不止一个量级。后端接口如果设计时本就无状态、Token鉴权直接复用一套API前端再开发小程序版效率极高。另一个是图片存储优化本地磁盘存照片在小规模场景没问题但如果照片多起来走OSS/COS这类对象存储才是正解加个CDN还能缓解访问压力。再就是数据看板对救助站管理员来说能一眼看出“本月新增流浪动物多少只”“领养成功多少只”“物资缺口哪些最多”比单独查表效率高太多。我自己踩过最大的一次心态坑是调试MyBatis缓存。MyBatis的二级缓存默认关一开就有一堆缓存一致性问题。救助站系统中数据修改频繁尤其申请单状态时时变开二级缓存反而让数据显示不准。所以默认真诚建议这个体量的项目MyBatis一级缓存默认开着就够了二级缓存别碰省下的那点数据库压力换来的是数据一致性的坑不划算。最后再分享一个小技巧开发阶段前端用Vue CLI的devServer代理后端接口文档直接配一个Swagger访问/swagger-ui.html就能看到所有接口。每次改完接口一刷新就能同步文档给测试同学和前后端对接省出来的时间不是一点半点。这俩搭配起来整个开发节奏能畅通很多。希望这套完整记录能帮到你折腾前后端分离的过程值得好好享受。
返回列表