
1. 项目整体设计思路与技术选型先说结论这个“基于web技术的流浪宠物救助网站”本质上是一个典型的Spring Boot Vue前后端分离项目。表面看是公益性质的信息平台但从技术角度看它把 Web 开发里最常见也最核心的那些点全占齐了——用户认证、角色权限、文件上传、状态机流转、检索分页、前后端联调再加上一层公益场景独有的业务规则。你要是能把这类项目的设计逻辑捋顺往后做类似的管理系统、信息平台基本就是同一套打法的换皮。1.1 技术栈选型为什么是 Spring Boot Vue先讲技术选型的底层逻辑。用 Spring Boot 做后端不是因为“大家都用所以我也用”而是它解决了这个项目最核心的问题快速搭建稳定可用的服务端骨架。内置 Tomcatspring-boot-starter-web一个依赖就把 Web 环境拉起来了不用像 SSM 时代那样手写一堆 XML 配置自动装配机制把数据源、事务、日志这些基础设施全部规范化你只需要关心业务代码生态成熟MyBatis-Plus、Sa-Token、Redis 这些工具类库几乎是无缝集成开发效率比从零写底层高太多。前端选 Vue核心原因是组件化和响应式特性非常适合这类信息展示加表单操作的场景。页面组件拆起来很灵活比如宠物卡片列表、领养申请表单、后台数据看板各自独立维护互不干扰。我也看到不少教程里推荐用 JSP 加 Bootstrap 做这个项目不是说不行但你要清楚 JSP 方案的服务端渲染模式在前后端联调、移动端适配、后续维护上会明显吃力。既然标题定了springboot-vue就严格走前后端分离前端一套 Vue 应用后端一套纯 API 服务两边各干各的。前后端分离带来的直接好处有三个一是后端接口可以复用以后做小程序或者 App 端直接调同一套 API二是前端开发和后端开发可以并行推进不需要互相等三是部署灵活前端静态文件扔到 Nginx 就行后端独立跑在服务器上互不干扰。1.2 功能模块拆解与用户流程设计流浪宠物救助网站核心用户是三类角色访客、注册用户、管理员。围绕这三类角色业务功能可以拆成四个大模块。前台信息展示模块这是网站的门面。访问者进来可以看到流浪宠物列表按宠物类型、所在城市、健康状况筛选查看宠物详情页包括照片、发现地点、救助人留言等。用户中心模块注册登录之后用户可以发布流浪宠物信息、提交领养申请、收藏感兴趣的宠物、查看自己的申请进度。这里要设计“用户-宠物-申请”的关联关系一个用户可以发布多只宠物也可以申请领养多只宠物但同一只宠物同时只能有一个进行中的领养申请。后台管理模块管理员登录后可以审核用户发布的宠物信息管理领养申请审核通过或驳回还能管理用户账号、发布系统公告。整个后台是一套完整的 CRUD 操作前端用表格组件加弹窗表单就够。寻宠与捐赠辅助模块这是加分项。寻宠启事解决的是“宠物走失”场景捐赠功能记录用户对救助站的物资或资金支持。这两个功能不需要很复杂但有它们能让项目在答辩或展示时更完整。用户流程上最核心的一条链路是用户注册登录 → 管理员审核发布宠物信息 → 浏览者提交领养申请 → 管理员确认领养 → 状态更新。这条链路设计好了整个项目的地基就打牢了。2. 数据库设计与核心表结构数据库设计是这类项目的灵魂。表结构如果设计得稀烂后面写代码会各种别扭。我建议按业务实体拆分表宁可多拆一张关联表也不要把字段堆在一张表里。2.1 核心业务表设计基于业务模块核心表我设计了七张覆盖用户、宠物、申请、收藏、公告这些核心实体。用户表sys_user字段名类型说明idbigint主键IDusernamevarchar(50)用户名唯一索引passwordvarchar(100)BCrypt加密后的密码nicknamevarchar(50)昵称phonevarchar(20)手机号roletinyint角色1-管理员 2-普通用户statustinyint状态0-禁用 1-正常created_atdatetime注册时间密码字段必须加密存储这是安全底线。我见过不少学生项目直接明文存密码答辩时老师一查数据库就露馅。流浪宠物表pet字段名类型说明idbigint主键IDnamevarchar(50)宠物名字typetinyint类型1-猫 2-狗 3-其他gendertinyint性别1-公 2-母agevarchar(20)年龄描述health_statusvarchar(255)健康状况描述cityvarchar(50)所在城市addressvarchar(255)发现地址imagestext图片路径多张用逗号分隔descriptiontext详细描述statustinyint状态0-待审核 1-待领养 2-已申请 3-已领养 4-已下架publisher_idbigint发布人IDcreated_atdatetime创建时间status 字段是整个业务的核心状态机后续所有领养逻辑都要围绕它做判断。领养申请表adoption_apply字段名类型说明idbigint主键IDpet_idbigint宠物IDuser_idbigint申请人IDreasonvarchar(500)申请理由experiencevarchar(500)养宠经验addressvarchar(255)居住地址statustinyint状态0-待审核 1-已通过 2-已拒绝 3-已取消apply_timedatetime申请时间audit_timedatetime审核时间audit_remarkvarchar(255)审核备注收藏表、公告表、寻宠启事表、评论表这四张属于辅助功能结构相对简单核心字段就是关联外键加内容字段这里就不逐一展开了。2.2 状态流转设计从发现到领养的全链路表设计完之后最关键的是把状态流转规则定清楚。这个环节很多人忽略导致写业务逻辑时到处是if...else越写越乱。我画一个逻辑流转图帮你理解不用工具画靠文字描述用户发布宠物信息初始状态是0-待审核管理员在后台审核通过宠物状态变成1-待领养如果审核驳回状态变成4-已下架用户看到待领养的宠物提交领养申请此时宠物状态从1变成2-已申请同时申请表的状态是0-待审核管理员审核申请通过的话宠物状态变成3-已领养申请表变成1-已通过拒绝的话宠物状态回到1-待领养申请表变成2-已拒绝用户自己撤销申请则申请表变成3-已取消宠物状态回到1-待领养。这套规则用一张表或者枚举类定义清楚业务代码里就不再散落魔数。做了这个设计写接口的效率会高很多因为你不需要在 Service 层反复纠结数据状态。注意一个关键点同一只宠物在“待审核”和“已申请”状态下都不能再次被提交领养申请。这个需要在申请接口里做幂等校验否则用户快速多次点击就会产生同一只宠物被多人同时申请的数据脏问题。3. 后端核心模块实现后端这一块我按“认证授权 → 宠物管理 → 领养申请”三个主链路来讲。理解了这三块剩下公告、收藏、评价都是类似的 CRUD 模式。3.1 认证授权模块JWT 登录态管理项目采用 JWTJSON Web Token做登录认证。为什么不用传统的 Session因为前后端分离架构下前端可能部署在 Nginx后端是独立服务Session 的跨域共享会很麻烦。JWT 是无状态认证后端不需要保存登录状态只需要在请求头里带一个 Token后端验签通过即可。核心操作分成三步第一步引入依赖和工具类。在pom.xml中加入jjwt依赖然后封装一个 JWT 工具类提供生成 Token 和解析 Token 的方法。Token 的过期时间我建议设置为 24 小时普通用户场景足够了。// JwtUtil.java 核心逻辑 public class JwtUtil { // 密钥生产环境务必放到配置文件中不要硬编码 private static final String SECRET_KEY your-secret-key; // 过期时间 24 小时 private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; public static String generateToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }第二步写登录接口。用户提交用户名和密码后后端从数据库查出用户用BCryptPasswordEncoder.matches()校验密码是否匹配。匹配成功就生成 Token 返回给前端前端存到 localStorage 或 Pinia 状态管理中。第三步写拦截器或者过滤器。在 Spring Boot 里实现HandlerInterceptor在preHandle方法里从请求头Authorization中取出 Token解析失败就返回 401解析成功就把用户信息放到ThreadLocal或RequestContext中方便后面的接口直接获取当前用户。拦截器只拦截需要登录的路径比如/api/user/**、/api/apply/**像/api/pet/list、/api/home/**这些公开接口要放行否则访客就没法浏览宠物信息了。3.2 宠物发布与图片上传宠物发布是整个系统里最常被操作的业务功能。前端用表单提交除了文本字段最麻烦的就是图片上传。设计上我建议图片和业务数据分开处理用户先调用/api/upload接口上传图片后端返回图片的访问 URL前端拿到 URL 后再和宠物表单数据一起提交到/api/pet接口后端的 PetController 接收整个Pet对象把图片 URL 存到images字段里多张图片用逗号拼接。文件上传接口要注意几个细节大小限制Spring Boot 中需要配置spring.servlet.multipart.max-file-size和max-request-size建议单张图片不超过 5MB一次请求不超过 20MB存储路径建议上传到服务器的独立目录下比如/data/pet-images/然后通过配置映射成静态资源访问路径。不要把图片存到项目源码的resources目录下否则重新部署时图片就丢了文件名校验不要直接用用户上传的原始文件名用UUID.randomUUID()生成新文件名防止文件名冲突也防止非法字符注入。代码实现里FileUploadController的核心逻辑就是接收MultipartFile校验文件类型只允许 jpg、png、gif、webp生成新文件名写到目标目录然后返回访问 URL。3.3 领养申请的业务闭环实现领养申请是业务规则最复杂的一个模块也是最容易写出 Bug 的地方。先看提交申请的逻辑。前端把宠物 ID 和申请理由、养宠经验等个人信息提交到后端后端要做三件事校验宠物是否处于“待领养”状态否则直接抛业务异常校验当前用户是否已经申请过这只宠物避免重复提交保存申请表同时把宠物状态从“待领养”改为“已申请”。Transactional(rollbackFor Exception.class) public void submitApply(ApplyDTO dto, Long currentUserId) { // 1. 查询宠物信息 Pet pet petMapper.selectById(dto.getPetId()); if (pet null) { throw new BusinessException(宠物不存在); } if (pet.getStatus() ! PetStatus.WAIT_APPLY.getCode()) { throw new BusinessException(该宠物当前不可申请领养); } // 2. 校验是否重复申请 Integer count applyMapper.selectCount(new LambdaQueryWrapperAdoptionApply() .eq(AdoptionApply::getPetId, dto.getPetId()) .eq(AdoptionApply::getUserId, currentUserId) .ne(AdoptionApply::getStatus, ApplyStatus.CANCEL.getCode())); if (count 0) { throw new BusinessException(您已申请过该宠物请勿重复申请); } // 3. 保存申请 更新宠物状态 AdoptionApply apply new AdoptionApply(); BeanUtils.copyProperties(dto, apply); apply.setUserId(currentUserId); apply.setStatus(ApplyStatus.WAIT.getCode()); apply.setApplyTime(new Date()); applyMapper.insert(apply); pet.setStatus(PetStatus.APPLYING.getCode()); petMapper.updateById(pet); }这里注意Transactional注解因为涉及两张表的更新操作必须保证事务性要么全部成功要么全部回滚。否则会出现“申请记录了但宠物状态没改”的脏数据情况。管理员审核的逻辑是对称的审核通过就更新申请表状态为已通过宠物状态改成已领养审核驳回就把申请表状态改成已拒绝宠物状态退回待领养。同样需要事务保证。4. 前端核心模块实现前端用 Vue 3 加 Element Plus 组件库这个组合在做这类信息管理型项目时效率很高。4.1 Vue 项目搭建与环境配置用 Vite 脚手架创建项目npm create vitelatest pet-frontend -- --template vue cd pet-frontend npm install npm install vue-router4 pinia axios element-plus项目创建好之后我建议先把目录结构分好src/api存放所有接口请求函数按模块拆文件比如pet.js、user.js、apply.jssrc/router路由配置包含动态路由和导航守卫src/storePinia 状态管理存用户信息src/views页面视图按功能分文件夹src/utils封装的 Axios 实例和工具函数。环境配置上vite.config.js里需要配置开发环境的代理。因为前端跑在localhost:5173后端跑在localhost:8080直接请求接口会跨域。代理配置如下export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/pet/list就会被代理转发到后端http://localhost:8080/api/pet/list开发阶段不需要后端处理 CORS。生产环境部署时这个代理不生效。通常的做法是 Nginx 里配置反向代理把/api路径转发到后端服务前端静态资源和后端 API 共用同一个域名从根源上规避跨域问题。这个点也常被面试官追问。4.2 Axios 封装与接口对接Axios 一定要封装不要在每个页面里直接裸调axios.get()。我习惯封装成一个实例统一处理 baseURL、超时时间、请求头注入和响应拦截器。// src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带 Token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.message || 网络错误) return Promise.reject(error) } ) export default request导航守卫是前端权限控制的关键。未登录用户只能访问首页、宠物列表、宠物详情、登录注册这些公开页面进入用户中心或者提交领养申请时要检查本地有没有 Token没有就跳登录页。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { ElMessage.warning(请先登录) next(/login) } else { next() } })4.3 核心页面逻辑筛选、详情、申请表单宠物列表页是整个网站的流量入口。我设计了三个筛选维度宠物类型猫/狗/其他、城市、状态看待领养还是全部筛选条件直接拼成参数传给后端接口。这里有个优化细节使用watch监听筛选条件变化变化了就重新请求列表数据同时要处理防抖避免用户切换筛选条件时频繁发请求。watch(filterForm, () { clearTimeout(timer) timer setTimeout(() { loadPets() }, 300) }, { deep: true })宠物详情页要展示的信息比较多轮播图、基本信息、救助人留言、领养要求。下方分两种情况处理如果宠物状态是待领养展示“我要领养”按钮如果是已申请或已领养按钮置灰并显示状态标签。这个交互逻辑跟着后端的状态字段走前端只需要根据pet.status控制即可。领养申请表单页使用 Element Plus 的el-form做校验。弹窗或独立页面提交后前端提示“申请已提交请耐心等待管理员审核”然后跳转到“我的申请”列表页。这里要提醒用户点击提交按钮后要做防重复提交处理通过按钮的loading状态控制防止用户手抖点两次后端做了幂等校验但前端体验层面也要防住。5. 常见问题与排查技巧实录这类项目做完之后踩坑最多的地方往往不是复杂的业务逻辑反而是那些看起来很简单的基础配置。我把自己实际开发中遇到的高频问题整理了一下做成一个速查表希望能帮你少走弯路。5.1 典型问题速查表问题现象根本原因解决方案前端访问后端接口报跨域错误开发环境未配置代理或生产环境未配置 Nginx 反向代理开发用 Vite proxy生产用 Nginx 把/api转发到后端图片上传成功但前端看不到图图片访问路径没映射到静态资源目录或 URL 前缀拼错确认WebMvcConfigurer中映射了虚拟路径地址格式要带/images/前缀前端传了 Token 但接口报 401拦截器解析 Token 失败密钥不一致或者 Token 过期检查 JWT 密钥是否配置一致确认 Token 未过期列表页搜索条件多SQL 报错MyBatis-Plus 条件构造器用错比如eq传了空字符串用StringUtils.isNotBlank()判断后再拼查询条件时间字段显示差 8 小时后端时区未配置为 UTC8Jackson 序列化格式不对配置spring.jackson.time-zone: GMT8和格式或统一使用LocalDateTime部署后刷新页面 404前端路由是 history 模式Nginx 缺少 try_files 配置在 Nginx server 块中添加try_files $uri $uri/ /index.html;上传大文件报错提示连接中断max-file-size配置过小或 Nginx 的client_max_body_size未设置同时调整 Spring 配置和 Nginx 配置的请求体大小限制5.2 部署环境下的几个坑第一个坑数据库连接的编码问题。生产环境 MySQL 连接字符串一定要加上characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai否则插入中文数据时极大概率出现乱码或Incorrect string value的报错。很多新手在本地开发时数据库编码设的是 utf8mb4 没问题换到服务器上的 MySQL 就炸了就是因为建库时没指定字符集。第二个坑图片访问权限。本地开发时图片路径直接映射到本地目录没问题但部署到服务器后要确保 Nginx 对静态图片目录有读取权限常见错误是 Nginx 用户对/data/pet-images/目录没有r-x权限导致图片 403。我之前遇到过这个问题排查半天才发现是权限问题解决方式就是给目录加上chmod -R 755。第三个坑后端日志定位业务问题太慢。建议在 Service 层的核心方法里加上日志输出用log.info记录关键操作比如申请领养、审核通过、权限校验失败等。出了 Bug 之后先看日志再猜原因效率会翻倍绝对不要用System.out.println来打日志。5.3 几个我在实操中养成的好习惯接口返回结构一定要统一。我习惯所有接口返回统一的ResultT结构包含code、message、data三个字段。前端拦截器只看code等于 200 就取data否则弹message。这个规范如果一开始不定好后面每个页面就要单独处理异常那酸爽谁写谁知道。数据库字段统一用下划线命名实体类用驼峰。MyBatis-Plus 默认开启驼峰映射这样数据库和 Java 实体之间省去大量手写映射的功夫。比如数据库字段publisher_id自动映射到 Java 的publisherId不需要额外注解。前端组件复用越早上越好。我一开始图快每个页面单独写表格和表单代码导致后期修改样式要动十几个文件。后来全部抽成PetCard、ImageUpload、StatusBadge这类子组件改一个地方全站生效效率天差地别。最后再分享一个自己做这个项目的体会流浪宠物救助网站表面上是 CRUD但真正有价值的地方在业务状态流转设计上。把流程理清楚了代码层面的实现反而不难。如果你打算在这类项目上深入一点可以尝试加入消息通知模块——申请人提交申请后管理员登录后台能收到待办提醒这会让你对“事件驱动”的理解上一个台阶。另外宠物图片加上缩略图处理、增加地图展示救助地点都是不错的扩展方向。