做后台系统最怕的是什么不是代码写不出来而是业务捋不清楚。尤其是“流浪宠物救助系统”这类项目表面上是个信息展示平台骨子里其实是一套多角色协作流程谁发布救助线索谁管理宠物档案谁审核领养申请谁去做回访记录每一步都得有人管。如果一开始没想明白角色边界后面写代码就是反复返工改接口、加字段、再调权限整个开发周期直接被拖垮。这篇文章要聊的就是一个基于SpringBoot Vue的“三个角色”流浪宠物救助系统完整实现。管理员、救助站、普通用户三端打通覆盖宠物发布、领养申请、审核流转、救助记录、回访反馈这条主线。我会把数据库怎么设计、状态怎么流转、前后端权限怎么控制、部署有哪些坑一条条拆开讲。不管你是正在做毕业设计还是想上手练一练企业级前后端分离开发这套思路都能直接抄作业。1. 项目整体设计与角色权限拆解1.1 救助系统到底需要解决什么很多新手拿到这类项目第一反应是做一个“宠物信息列表”上传几张流浪猫狗的照片用户点点就能申请领养完事。但真到上线跑起来问题马上就出来了同一个宠物可能被两三个人同时申请领养救助站的人不知道该审哪个申请通过之后没人跟进到底有没有去领走宠物是不是被善待再往后用户上传的救助线索没有归属志愿者干了哪些活、救助效率怎么样完全是一笔糊涂账。所以这个系统真正要解决的不是“展示宠物”而是“让救助这件事可追溯、可管理”。权限设计也必须服务于业务流程而不是简单粗暴地“给不同人看不同页面”。三个角色的划分本质上是把救助链条切成了三段管理链条管理员、执行链条救助站/志愿者、需求链条普通用户/领养人。1.2 三个角色的职责边界先看角色分工这是我最初设计系统时反复调整过的部分。最开始的方案里我打算塞五个角色后来砍成三个因为每多一个角色前端路由、后端拦截、表设计都要跟着复杂一层。角色核心职责关键权限管理员系统配置、用户管理、数据统计、全局监管用户禁用/启用、审核救助站申请、查看全站数据救助站宠物信息发布、领养审核、救助记录、回访反馈宠物增删改、领养申请通过/拒绝、填写回访单普通用户浏览宠物、提交救助线索、发起领养申请浏览宠物列表、申请领养、上报流浪宠物线索有一点值得特别说不要把“救助站”和“个人志愿者”拆成两个角色。实操中两者做的事几乎完全一样分开只会让领养审核逻辑和宠物归属关系加倍复杂。我用“救助站”一个角色里面通过一个station_name字段区分归属够用。1.3 为什么是SpringBoot Vue而不是别的搭配技术选型方面我的建议很直接后端SpringBoot前端Vue数据库MySQL鉴权用JWTUI库直接上Element UI。这套组合是“最不折腾”的默认解。SpringBoot的好处在于它把Spring的老套路封装得极其舒服。你要做REST接口直接RestController要做参数校验Valid贴在参数上就行要做统一异常处理RestControllerAdvice接住所有异常。对于“救助系统”这种业务模型清晰、接口数量中等偏上的项目SpringBoot的上手成本和学习收益比非常划算。Vue这边三个角色的界面差异用路由和组件隔离来实现。注意不是写三套前端而是同一套前端在路由层做动态加载在按钮层级做权限控制。这样用户在登录后拿到自己的角色前端会根据角色渲染对应菜单和按钮页面看起来“像是三个系统”实际上共用一个工程。后续要改样式、改接口一次搞定不用维护三份代码。这套选型还有一个隐藏优点招聘市场上会SpringBoot和Vue的人很多团队协作或者遇到问题搜资料都很容易。2. 数据库设计与核心模块拆解2.1 表结构设计重点在状态字段数据库是整个救助系统的地基这里设计错了后面改的成本最高。我最终定下来的核心表有这么几张sys_user用户表字段包含username、password、role区分管理员/救助站/普通用户、phone、enabled是否被禁用、station_id救助站关联字段。pet宠物信息表字段包含name、type猫/狗/其他、gender、age、vaccine_status、sterilize_status、health_status、image_url、pet_status、station_id、create_time。rescue_record救助线索记录表记录reporter_id、location_desc、image_url、description、status、station_id。adoption_application领养申请表记录application_no、pet_id、applicant_id、applicant_name、reason、status、audit_time、audit_remark。adoption_record领养回访记录表记录application_id、pet_id、adopter_id、adopt_time、visit_date、visit_result、visit_remark。notice公告表管理员发布平台公告用于简单的内容管理。这里最关键的设计是几乎每张表都带一个status字段并且我用tinyint类型存数字状态而不是直接存中文。比如宠物状态0待审核, 1待领养, 2已领养, 3救助中领养申请状态0待审核, 1已通过, 2已拒绝, 3已领养, 4已取消。为什么不用字符串因为字符串状态容易打错字而且业务流转时不好做状态机的整数比较。数字状态配合枚举类或者字典常量代码可维护性会好很多。2.2 宠物信息从“救助”到“领养”的生命周期一只流浪狗从被普通用户上报到最终进入家庭在系统里经历的路径大概是这样的普通用户提交救助线索填写地点、描述、照片状态变为救助待处理。管理员或救助站看到线索后判断是否需要出勤若确认受理可将其转为“救助站已接收”同时创建宠物档案。宠物档案创建后经过体检、驱虫、绝育状态变为待领养前端宠物列表开始展示。用户提交领养申请进入审核流程。救助站审核通过线下完成交接用户在系统中确认已领养状态变为已领养。救助站定期填写回访记录系统留存。这套生命周期的好处是任意时刻都能回答一个问题“系统里现在有多少宠物在哪个阶段”这也是管理员首页做统计表的数据来源。2.3 为什么领养申请一定要单独建表我见过很偷懒的设计直接在pet表加一个applicant_id字段用户申请后把ID填进去。这个做法看着省事实际是灾难被拒了怎么办同时三个人申请怎么办历史记录怎么查领养申请单独成表等于把“谁在什么时候申请了什么宠物、审核结果是什么、备注是什么”完整存档。一张表天然支持一个宠物对应多个申请我们只需要在业务逻辑上控制“同一宠物只能有一个处于待审核状态的申请”剩下的流程都能顺畅走通。实际操作中我还在adoption_application表上加了唯一索引约束pet_id applicant_id下保证同一用户对同一宠物只能有一条有效记录防止重复提交刷屏。3. 后端关键实现权限控制与业务闭环3.1 基于JWT的登录和角色鉴权登录鉴权这块我直接选择了JWT SpringBoot拦截器没有上Spring Security。原因很简单这个系统的安全性要求没到“强制用框架”那个级别Spring Security配置复杂学习成本高而拦截器短小精悍够用。登录接口的逻辑用户提交username和password后端查库用BCrypt对密码做比对。比对通过后根据用户角色生成一个JWT字符串subject放用户IDclaim里放role设置过期时间我给的是24小时。返回给前端后前端每次请求在请求头携带Authorization: Bearer xxx。拦截器做的事情是解析Token校验签名和过期时间把用户ID和角色放入ThreadLocal供Controller使用。代码结构大致是这样public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); // 校验token逻辑 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); UserContext.set(claims); return true; } }角色权限判断我直接用自定义注解RequireRole(admin)贴在Controller方法上拦截器拿到请求路径和注解比对当前用户的角色。这样管理员接口、救助站接口、用户接口各归各的权限空间逃都逃不掉。3.2 领养审核状态机的完整闭环领养申请的状态流转是整个系统里最容易出错的地方也是当年我调试时间最长的一段。简单来说它的规则是用户提交申请状态为待审核。救助站处理可以选择通过或者拒绝。如果通过状态到已通过此时系统自动生成一条adoption_record领养时间留空等待线下交接确认。用户线下领养成功后救助站在系统中点击“确认领养”状态变为已领养。如果拒绝必须填写拒绝理由用户可以再次申请这就要靠之前说的唯一索引控制有效记录。这套状态机的核心是Controller层禁止直接修改状态值必须通过Service方法比如auditApplication(applicationId, approverId, result, remark)去流转。这样哪怕以后加新状态改动也只集中在Service方法里不会出现前端传一个status字段直接把数据库改乱的情况。public void auditApplication(Long applicationId, Integer result, String remark) { AdoptionApplication application getById(applicationId); // 只有待审核状态才允许审核 if (!application.getStatus().equals(StatusConstant.APPLY_PENDING)) { throw new BusinessException(当前状态不允许审核操作); } application.setStatus(result); application.setAuditTime(LocalDateTime.now()); application.setAuditRemark(remark); updateById(application); }3.3 图片上传别把路径存错救助线索、宠物相册都涉及图片上传。我在这个项目里踩过的最大坑是图片存的是全路径http//localhost:8080/upload/xxx.jpg结果换环境部署后所有图片全部裂掉。最终建议数据库只存相对路径比如/upload/20240321/xxxx.jpg前端展示的时候前面拼上服务器的域名或IP。上传的物理目录不要放在项目内部放到一个独立的/data/pet-upload/文件夹SpringBoot通过配置项file.upload-dir读取Nginx再把/upload/下的请求映射到这个目录。这么做的原因是项目打成jar包之后项目内部的临时目录很容易在重启时被清掉图就没了。上传接口本身不复杂用MultipartFile接收即可重点是把文件类型和大小校验做掉。图片类型我限制为jpg、png、webp大小限制为5MB超出直接抛异常避免有人传视频或者超大图把磁盘塞满。4. 前端Vue三端角色的落地实现4.1 目录结构与路由思路前端我用的是Vue CLI Vue Router Vuex Axios没有上比较重的权限框架但把菜单权限、路由权限、按钮权限都做了。目录结构经验如下图文字描述src/views/admin管理员页面用户管理、统计看板、救助站审核。src/views/station救助站页面宠物管理、申请审核、回访记录。src/views/user普通用户页面宠物列表、我的申请、救助上报。src/store/modules/user.js存储用户信息、角色、动态菜单。src/router/index.js维护所有路由每个路由的meta里加roles字段。这样划分之后每个角色的代码位置清晰改动不会互相污染。要注意的是不要真的写三套layout布局组件共用一个只是菜单项根据角色渲染不同。4.2 路由守卫控制角色访问路由部分核心是router.beforeEach。用户未登录时一律跳登录页登录后如果访问的页面meta.roles不包含当前角色跳转到一个403页面。代码大概是router.beforeEach((to, from, next) { const token store.getters.token if (!token) { if (to.path /login) return next() return next(/login) } if (to.path /login) return next(/) const roles store.getters.roles if (to.meta.roles !to.meta.roles.includes(roles)) { return next(/403) } next() })按钮级权限我在指令v-permission里做原理是检查用户角色在不在按钮要求的角色列表里不在就把DOM节点移除。4.3 角色首页的差异设计三个人的首页不能一样否则权限管理就只是表面工作。管理员首页放的是统计卡片今日新增救助线索数、待审核领养申请数、在册宠物数量、累计领养成功数。这些数值由后端聚合接口一次性返回前端图表只是展示。救助站首页突出“待办”待处理的领养申请列表放在最显眼的位置下面是我负责的宠物状态分布。前端用了一个简单的卡片列表而不是复杂图表因为救助站的人在实际操作中更关心“有没有要审的”而不是趋势分析。普通用户首页就是宠物展示和快速救助入口。宠物卡片组件包含形态、年龄、健康状态右上角直接挂“申请领养”按钮。页面逻辑很简单但用户反馈最好的是优化项列表分页加上关键词搜索可以按猫/狗/其他筛选。这种小细节对体验提升非常明显。4.4 Axios封装与Token处理前端请求封装我统一放在src/utils/request.jsAxios实例设置baseURL、timeout15000请求拦截器里从Vuex取token并设置到请求头响应拦截器统一处理后端返回结构。后端约定返回格式为{ code: 200, message: success, data: {...} }响应拦截器里只要code ! 200就弹错误消息不用每个页面都写一遍错误判断。遇到401时逻辑是清理本地用户状态并跳转登录页。这部分值得注意的是跨域。开发环境我通过Vue CLI的devServer.proxy把/api代理到后端上线环境靠Nginx反向代理所以前端代码不需要写完整后端地址所有请求都以/api开头环境切换时不需要改代码。5. 联调、部署与常见问题排查5.1 前后端字段对接最容易踩的坑联调过程中最大的坑是时间值。后端返回LocalDateTime默认序列化成“2024-03-21T153000”这样带T的格式前端直接显示在页面上很难看。我处理的方法是全局配置Jackson序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8还有一个高频问题是Long类型的ID精度。数据库主键如果用了雪花ID或自增ID超过JS最大安全整数前端拿到的ID精度会丢失导致详情页查不到数据。解决方式是在后端配置里把Long类型用JsonSerialize(using ToStringSerializer.class)转成字符串或者统一改用字符串ID。我推荐简单一点数据库主键直接用自增然后输出时转字符串。5.2 部署架构与Nginx配置部署时的结构是前端打包成静态文件交给Nginx后端打jar包用systemd或nohup java -jar运行。Nginx配置里要做两件事一是把/映射到前端dist目录二是把/api/反向代理到后端8080端口同时处理图片路径/upload/。server { listen 80; server_name your-domain.com; root /opt/pet-frontend/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /upload/ { alias /data/pet-upload/; } location / { try_files $uri $uri/ /index.html; } }注意proxy_pass后面的斜杠如果写http://127.0.0.1:8080不写斜杠请求会变成/api/user/list转发到后端写了斜杠/api会被剥掉后端收到的是/user/list。这里的规则要看后端接口有没有统一/api前缀我建议后端接口都以/api开头Nginx这里不吞掉前缀也就是proxy_pass不加末尾斜杠匹配规则最直观。5.3 常见问题速查表把这几天联调遇到的高频问题做个总结现象原因解决办法登录后刷新页面就回到登录页Vuex状态没有持久化token丢失引入vuex-persistedstate或把token存localStorage图片上传成功后页面显示裂图数据库存的是本地全路径环境切换失效改为相对路径前端拼接服务器地址前端请求出现CORS错误线上Nginx未正确代理或后端未放开跨域Nginx代理解决开发环境用devServer proxy审核领养申请后列表状态不刷新前端调接口后未重新拉取数据审核成功后await fetchList()刷新当前页数据导出Excel时报中文乱码后端未设置Content-Type为带字符集响应头设置application/vnd.ms-excel;charsetutf-8宠物列表翻页后筛选条件丢失查询参数单独管理翻页时未带上条件请求参数对象统一保存到data中这里我特别想展开说第一个问题。很多新手以为登录后把token放进Vuex就完事了但刷新页面后Vuex重新初始化状态全丢。我是在main.js里做了一次本地读取用vuex-persistedstate插件把用户信息和token持久化到localStorage这样刷新后路由守卫能立刻拿到token体验就顺了。5.4 部署时数据备份和恢复的经验上线前一定要做一次全量数据导出我用的是MySQL的mysqldump命令定时任务每天凌晨执行一次备份文件保留最近7天。这套系统虽然是“救助”主题但数据仍然很重要尤其是领养记录和回访记录丢了就没了补都补不回来。实际运维时我还加了一个小脚本每天检查磁盘空间和数据库连接数。前端项目不大数据库量也不会特别大但防止图片占满磁盘还是很有必要的。最后再分享一个我实际操作中的小习惯项目做完后我会模拟三个角色各注册一个账号跑一遍完整流程。用户提交救助线索管理员受理救助站建立宠物档案并上架申请人提交领养救助站审核通过然后记录回访。这一圈跑通了才能说明系统的核心链路没问题。看完这篇文章你照着这套思路去搭基本一天之内就能把骨架立起来剩下的事情就是按业务需求逐步填充细节。每个项目的业务会有差异但“角色先行、状态机做底、权限贯穿前后端”这套方法论是可以直接复用的。