1. 项目概述与业务定位
1.1 这个平台到底要解决什么问题
动物领养这件事,听起来简单,真正做起来却处处是坑。民间救助站和宠物医院每天都会收到大量流浪动物,靠朋友圈转发和线下摆摊的效率实在太低。有人想领养找不到渠道,救助站想送养找不到靠谱的人,中间还夹杂着信息造假、领养后弃养等一堆问题。这套"基于SpringBoot+Vue的动物领养平台管理系统"就是冲着这些痛点去的。
我见过很多类似项目,大部分把重点放在"展示宠物列表 + 提交领养申请"这种最简单的流程上。说实话,那种项目做完自己心里都发虚,因为现实中领养流程远比这复杂。一个合格的领养平台至少要覆盖四个环节:宠物信息的公开与筛选、领养人在线申请、管理员线下审核与回访、领养后的状态追踪。这套系统的设计思路就是把这四个环节串成一条完整的业务闭环,而不是只做表面上的CRUD。
从技术角度看,项目用的是Java后端最常见的组合:SpringBoot提供应用框架,MyBatis负责数据库操作,MySQL存数据,Vue做前端页面。这个组合在目前的毕业设计和中小型项目中几乎是默认选型,不是因为有多先进,而是因为它足够成熟、资料多、踩坑记录全,一个人完全能在几周内把它跑起来并做出完整功能。
1.2 适合谁来学习和二次开发
这套代码的阅读门槛不高。如果你正在做毕业设计,或者刚学完Java和Vue想找个完整的全栈项目练手,这个项目是很好的参考对象。它的整体结构干净,前后端分离的目录划分明确,不会像老式JSP项目那样把页面和逻辑搅在一起。
要读懂并改造这个项目,你需要的基础包括:Java基础语法和面向对象概念、SpringBoot的基本注解(@Controller、@Service、@Mapper这类)、MyBatis的Mapper接口与XML配置方式、Vue组件化和Vue Router的基本用法、MySQL的建表与SQL语句。这些都不需要特别深入,但对Vue的组件通信和SpringBoot的依赖注入最好有个基本概念,否则看某些代码段会卡壳。
我个人不建议完全零基础的人拿着这套源码直接开跑。至少要先把Java和SpringBoot的HelloWorld跑通,再开始看这个项目,效率会高很多。项目里有一些值得反复看的细节,比如领养审核状态机的设计、文件上传与图片预览、MyBatis动态SQL处理多条件查询,这些内容比CV大厂的业务代码更贴近普通开发者的实际工作场景。
2. 技术选型与整体架构拆解
2.1 为什么是SpringBoot而不是Spring MVC
很多人看到SpringBoot和Spring MVC会犯迷糊,以为二者是对立关系。实际上SpringBoot只是把Spring家族的东西做了自动装配和约定优于配置的整合,底层依然是Spring MVC那套机制。这个项目选SpringBoot核心原因有两点。
第一是开发效率。传统SSM项目光是配置就要折腾很久:web.xml、spring-mvc.xml、spring-mybatis.xml、数据库连接池配置、事务管理配置,每个文件都要手写一大堆Bean定义。SpringBoot用自动配置把这些全部搞定,你只需要在application.yml里写数据库连接信息和几个自定义参数,就能把项目跑起来,省下来的时间可以安心写业务代码。
第二是部署方便。SpringBoot内置Tomcat,打出来的Jar包直接java -jar就能运行。这对部署到服务器、给别人演示项目非常友好,不需要单独装Tomcat再折腾War包发布。就算你完全不会Linux运维,也能用最简单的方式把系统跑起来。
2.2 Vue在前后端分离架构里扮演的角色
前端选用Vue而不是JQuery+Bootstrap那套老方案,核心原因是项目需要足够强的交互性。管理员审核领养申请时,需要在一个页面上看到用户提交的信息、宠物信息、历史记录,同时还要进行状态更新操作,这种场景下组件化开发优势明显。Vue把页面拆成组件,每个组件只管自己的数据和事件,维护起来思路非常清晰。
另一个关键点是路由管理。平台分为管理员端和普通用户端,两端的页面差异极大。管理员要看申请列表、宠物管理、回访记录管理,用户要看宠物展示、提交申请、查看进度。Vue Router配合路由守卫,可以在前端拦截未登录用户和管理员角色,保证页面访问权限。这套机制比传统的JSP标签权限控制要直观得多。
项目的前端构建工具我建议放在package.json里配置好,用npm管理依赖,npm install、npm run serve两步就能启动开发环境。前端请求用axios统一封装,把baseURL指向后端接口地址,统一处理错误码和token传递。这个封装方式值得多看几遍,因为所有前后端交互都走这一个入口。
2.3 MyBatis在数据访问层的定位
MyBatis在这套项目里负责的是"Java对象和数据库表之间的映射"。它比JPA/Hibernate更灵活,SQL完全由开发者自己控制,想怎么优化就怎么优化,适合那些对SQL有掌控欲、或者查询逻辑相对复杂的项目。
这个项目的宠物查询是有复杂度的:按名称模糊搜索、按种类筛选、按状态筛选、按时间排序,这些条件组合起来就是一个典型的动态SQL场景。MyBatis的<where>和<if>标签能很优雅地解决这个问题,不需要写多个不同SQL语句,也不需要在Java代码里手动拼接SQL,代码可读性和维护性都高很多。
表结构之间的关系映射也是MyBatis的强项。宠物表和领养申请表、用户表和领养申请之间都是典型的一对多关系。用MyBatis的association和collection标签做级联查询,一次把关联数据查出来,避免N+1问题。这里建议读者重点关注ResultMap的配置,这是MyBatis用来解决数据库字段名和Java属性名不一致问题的核心手段。
| 分层 | 技术选型 | 核心职责 |
|---|---|---|
| 前端展示层 | Vue + Vue Router + Axios + Element UI | 页面渲染、路由控制、接口请求、状态提示 |
| 后端控制层 | SpringBoot Controller | 接收请求、参数校验、调用Service、返回JSON |
| 业务逻辑层 | Service + ServiceImpl | 业务规则处理、事务控制、状态流转 |
| 数据访问层 | MyBatis Mapper接口 + XML | SQL编写、结果集映射、数据库交互 |
| 数据存储层 | MySQL 5.7+ | 持久化业务数据、保证数据一致性 |
3. 数据库设计与核心表结构解析
3.1 数据库设计的分层思想
数据库设计是整个项目的地基,地基没打好,后面写再多代码都是空中楼阁。这套项目的表结构设计,核心是围绕"用户、宠物、领养申请"三条主线展开的。
我先把整体表结构列出来,然后逐个拆解。
用户相关表:
user用户表:id、username、password、phone、email、avatar、role(ADMIN/USER)、status、create_timerole/user_role可选:如果要做精细的权限控制可以拆出来,当前系统用role字段搞定,简单项目不推荐过度设计
宠物相关表:
pet宠物表:id、name、type(猫/狗/其他)、breed、age、gender、health_status、description、images(多图用逗号分隔或单独建表)、status(待领养/审核中/已领养)、publisher_id、create_time
领养流程相关表:
adoption_application领养申请表:id、pet_id、user_id、apply_reason、experience、address、status(PENDING/APPROVED/REJECTED/FINISHED)、apply_time、handle_time、handler_idinspection_record回访记录表:id、application_id、inspection_time、inspection_result、suggestion、operator_id
辅助功能表:
message留言表:id、pet_id、user_id、content、create_timenotice通知表:id、title、content、create_time(管理员发布站内公告)operation_log操作日志表:id、user_id、action、target_type、target_id、create_time(审计用)
这个表结构看着简单,但每一张表的字段设置都有它的道理。举个最典型的例子:pet表的publisher_id字段。为什么要这个字段?因为很多平台允许用户自己发布待领养的宠物信息,而不是只有管理员能发。这个字段记录的是"这条宠物信息是谁发布的",在后续的修改权限控制、数据统计中都会用到。
3.2 领养申请状态机的设计
状态机是这套系统最核心的设计之一。领养申请不能像商品订单那样简单分为"待处理/已处理",它必须能表达整个领养流程的阶段性。
我设计的状态流转如下:
PENDING(待审核)→ APPROVED(审核通过,待回访) PENDING(待审核)→ REJECTED(审核拒绝) APPROVED(审核通过)→ FINISHED(回访合格,领养完成) APPROVED(审核通过)→ REJECTED(回访不合格,终止领养)这个状态机必须在后端的Service层强制控制,不能只在页面上让用户随便点。比如已经FINISHED的申请不能重新变成PENDING,REJECTED的申请不能直接变成FINISHED。实现方式就是在Service层的状态更新方法里做状态校验,不满足条件的直接抛业务异常。
还有一个细节值得注意:当某个领养申请状态为PENDING或APPROVED时,对应宠物应被标记为"审核中"或"已预约",不能被其他用户重复申请。这需要在提交申请时做并发控制。最简单有效的方式是SQL层面加条件:UPDATE pet SET status = 'APPLYING' WHERE id = ? AND status = 'AVAILABLE',如果影响行数为0,说明宠物已经被申请了,直接提示用户。这种方式比先查再更新的做法更安全,能避免并发场景下的超卖问题。
数据库表字段里我特意加了一个version字段或使用状态条件更新的方式,目的就是为了防止在高并发场景下同一只宠物被多人同时申请。很多初学者在做这种系统时会忽略这一点,到答辩或真正使用时被人抓出漏洞就晚了。
3.3 用户表与权限设计
user表用role字段区分管理员和普通用户,这个设计在小型项目中完全够用。如果以后要扩展更细粒度的权限(比如不同的管理员只能管理不同的模块),可以改用RBAC模型,拆出角色表、菜单表、角色菜单关联表。但就当前项目而言,过度设计反而会增加学习成本。
密码存储不使用明文。项目使用BCrypt加密,盐值自动混入密文,同一密码每次加密结果都不同,即使数据库泄露,攻击者也难以反向破解。用户注册时用BCryptPasswordEncoder的encode方法加密,登录时用matches方法校验。
登录成功后的会话保持有两套方案:传统Session和Token。我在这套项目里使用JWT Token方案,原因在于前后端分离架构下,服务端不保存会话状态,接口是无状态的。用户登录成功后,后端返回一个Token,前端每次请求在Header里带上这个Token,后端用拦截器统一校验。这样服务端不需要关心"哪个用户在线",只需要验证"这个Token是否有效、属于哪个用户"。
4. 后端核心业务与接口设计实现
4.1 后端项目结构与分层职责
后端项目的包结构采用标准的Controller-Service-Mapper三层架构。这里我把实际结构列出来:
com.pet.adoption ├── controller // 接收前端请求,返回统一结果 ├── service // 业务接口 │ └── impl // 业务实现类 ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体类 ├── dto // 数据传输对象,避免直接暴露实体 ├── vo // 视图对象,给前端返回定制化数据 ├── config // 配置类:拦截器、跨域、文件上传等 ├── utils // 通用工具类:JWT、日期、字符串等 ├── common // 统一返回结果、异常处理、常量定义 └── interceptor // JWT登录拦截器这种分层方式的优势在于各层职责清晰、相互独立。Controller层只负责参数接收和结果返回,不写业务逻辑;Service层承载核心业务逻辑,比如状态机校验、事务控制;Mapper层只写SQL和映射,不做业务判断。这样的代码读起来舒服,后期维护也轻松。
4.2 核心接口设计与业务逻辑实现
我挑几个核心接口来拆解,这些也是面试时最常被问到的点。
宠物管理相关接口
GET /api/pet/list宠物分页列表,支持按名称、种类、状态筛选,使用MyBatis动态SQL。返回的分页数据结构中包含total和records两个字段,前端即可用它们渲染分页组件。
POST /api/pet/add新增宠物信息。这里涉及图片上传问题,我的做法是前端先把图片上传到专门的接口,后端保存文件并返回文件访问URL,然后再把URL随宠物信息一起提交。图片存储到本机磁盘,通过一个配置的访问路径映射为静态资源。如果后续部署到云服务器,可以换成OSS或COS,逻辑是一样的。
PUT /api/pet/update更新宠物信息。这里要注意一个权限问题:普通用户只能修改自己发布的宠物,管理员可以修改全部。后端的实现是在Service层做判断:如果当前用户的角色不是ADMIN,且pet.publisher_id不等于当前用户ID,直接抛出无权限异常。这个权限判断不能依赖前端隐藏按钮,后端必须做校验,否则有人直接调接口就能篡改数据。
领养申请相关接口
POST /api/adoption/apply提交领养申请,是系统中业务逻辑最多的接口之一。流程如下:
@Transactional public void applyAdoption(AdoptionApplyRequest request) { // 1. 校验宠物存在且状态为AVAILABLE Pet pet = petMapper.selectById(request.getPetId()); if (pet == null || !"AVAILABLE".equals(pet.getStatus())) { throw new BusinessException("宠物不存在或已被领养"); } // 2. 校验用户是否已经有在途申请(防止重复申请) int applyingCount = applicationMapper.countApplyingByUser(request.getUserId()); if (applyingCount > 0) { throw new BusinessException("您有未完成的领养申请,请等待处理"); } // 3. 插入申请记录,状态为PENDING AdoptionApplication application = new AdoptionApplication(); application.setPetId(request.getPetId()); application.setUserId(request.getUserId()); application.setStatus("PENDING"); application.setApplyReason(request.getApplyReason()); application.setExperience(request.getExperience()); application.setContactAddress(request.getContactAddress()); applicationMapper.insert(application); // 4. 原子性更新宠物状态,防止并发重复申请 int rows = petMapper.updateStatusIfAvailable(pet.getId(), "AVAILABLE", "APPLYING"); if (rows == 0) { throw new BusinessException("手速慢了,宠物已被其他人申请"); } }这里有个非常容易忽略的细节:事务。整个申请过程涉及两步数据库写入(插入申请记录 + 更新宠物状态),必须放在同一个事务中。如果插入申请成功但更新宠物状态失败,事务回滚,不会产生"有申请但宠物状态没变"的数据不一致。我在Service实现类上加了@Transactional注解,并且在异常时主动抛出RuntimeException触发回滚。这一点在答辩时值得重点讲,是整个系统数据一致性的关键。
PUT /api/adoption/audit管理员审核申请。管理员的审核操作会将申请状态变为APPROVED或REJECTED。这里我加入了最重要的业务规则:当审核拒绝时,需要把宠物状态恢复为AVAILABLE,同时记录审核意见。审核通过时,宠物不立即变为已领养,而是进入"待回访"阶段,等待线下回访完成后才最终确认。这种设计贴近真实领养场景:线上审核只是初步筛选,线下回访确认才能保证宠物确实到了一个靠谱家庭。
POST /api/adoption/finish管理员录入回访结果。回访合格,申请变为FINISHED,宠物状态变为ADOPTED;回访不合格,申请变为REJECTED,宠物状态恢复为AVAILABLE。同时记录回访时间、回访结果和建议。
4.3 拦截器与统一异常处理
后端统一配置了一个JWT拦截器。拦截器会拦截除登录、注册、宠物公开列表等白名单之外的请求,读取请求Header中的Token,解析用户信息并放入当前请求上下文。如果Token缺失或过期,直接返回401状态码。
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); } if (!StringUtils.hasText(token)) { throw new UnauthorizedException("未登录或登录已过期"); } // 解析Token并存入ThreadLocal UserContext.set(JwtUtils.parseToken(token)); return true; } }统一异常处理使用@RestControllerAdvice注解,把业务异常、参数校验异常、系统异常分别处理,返回统一的JSON结构给前端。这样前端不需要在每个请求里单独处理异常分支,只要判断返回码即可。我在实际开发中会定义结果码:200成功、400参数错误、401未登录、403无权限、500系统错误,前端Axios拦截器里根据状态码统一提示,体验会好很多。
5. 前端页面设计与核心组件实现
5.1 前端整体结构与路由规划
前端项目的结构按Vue CLI标准划分,核心目录如下:
src ├── api // 所有接口请求方法,按模块分文件 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置与守卫 ├── store // Vuex状态管理(用户信息、Token) ├── views // 页面级组件 │ ├── admin // 管理员端 │ └── user // 用户端 ├── utils // 工具函数:request封装、token处理 └── App.vue // 根组件路由规划是整个前端设计的骨架。用户端路由包括首页宠物展示列表、宠物详情页、用户个人中心(我的申请、我的发布)、登录注册页;管理员端路由包括宠物管理、领养申请管理、回访管理、用户管理、公告发布。
路由守卫的逻辑很关键,它在每次路由跳转前执行统一校验,相当于前端的"第一道门"。我在router.beforeEach里做了两件事:检查目标路由是否需要登录才能访问;如果页面需要管理员权限,校验当前用户角色是否为ADMIN。验证不通过就跳转到登录页或首页。
5.2 宠物列表与详情页的实现细节
宠物列表页是整个平台的流量入口,它的UI展示直接影响用户体验。我用Element UI的卡片组件实现宠物卡片展示,每个卡片显示宠物照片、名称、种类、性别、年龄、状态标签。状态标签用不同颜色区分:AVAILABLE绿色、APPLYING橙色、ADOPTED灰色。底部有筛选栏,按种类、性别、状态筛选,顶部有搜索框支持名称模糊查询。
宠物详情页除了基本信息展示,还包含一个轮播图组件来展示多张宠物照片。左侧是图片,右侧是信息和操作区。操作区根据用户角色和宠物状态动态渲染:管理员看到的是"编辑"、"下架"按钮;普通用户看到的是"申请领养"按钮(宠物状态为AVAILABLE时),或者"查看申请进度"按钮(已提交过申请时)。这个动态渲染逻辑放在Vue的v-if里,通过判断用户角色和宠物状态来切换。
上传图片的组件我用的是Element UI的Upload上传组件,注意需要配置action属性指向后端上传接口,同时携带JWT Token在请求Header中。后端返回图片URL后,前端用fileList和imageUrl字段维护已上传图片的列表。
5.3 申请流程与个人中心的状态展示
用户提交领养申请时,会进入一个表单页面,需要填写领养原因、养宠经验、居住地址等字段。这里有一个细节:表单提交前必须做前端校验,比如原因不能为空、字数不少于10个字符,这样能避免无效申请进入后端。当然前端校验只是体验优化,真正的强校验在后端Service。
个人中心的"我的申请"列表是对用户最友好的进度反馈。整个生命周期都用时间线组件展示,从"提交申请"到"线上审核",再到"线下回访",最后"领养完成"。这样用户能清晰知道自己的申请进行到哪一步,也避免了反复打电话询问进度的运营压力。
Vuex负责存储用户基本信息与Token。用户登录成功后,我把用户信息commit到state中,同时写入localStorage,刷新页面时重新恢复。App.vue的created钩子里有一个初始化方法,从localStorage读取Token并校验有效性,如果有效就拉取最新用户信息。这样即使刷新页面,用户也不会被强制登出。
5.4 前端开发调试时的一个坑
使用Vue开发时,最常见的问题是跨域。开发环境下前端运行在localhost:8080,后端运行在localhost:8081,两者的端口不同,浏览器会拦截跨域请求。我在Vue项目的vue.config.js里配置了devServer的proxy代理,把/api前缀的请求转发到后端端口,这样浏览器的视角下所有请求都是同源的,不会触发跨域限制。
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } }生产部署时不需要这个代理,Nginx直接配置前端静态文件,并将/api请求反向代理到后端服务。这是下节要展开的部署内容。
6. 前后端联调与环境配置要点
6.1 数据库初始化与MyBatis配置
项目附带sql目录下的初始化脚本,包含建库建表和基础数据。基础数据里我准备了一个测试管理员账号和一个演示用户账号,方便直接登录体验。脚本里的表结构都加了字段注释,建议读者在Navicat或DataGrip中执行后先浏览一遍表结构和注释,对整个数据模型有个直观认识。
application.yml(或application-dev.yml)里有两个关键配置:数据源和MyBatis映射。
spring: datasource: url: jdbc:mysql://localhost:3306/pet_adoption?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.pet.adoption.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true这个配置解决了数据库create_time到Java属性createTime的自动映射问题。加上它之后,大部分情况下不需要手动写ResultMap的每个字段映射。开发阶段推荐打开StdOutImpl日志,控制台会打印每条执行的SQL和参数,排查问题效率高很多。
6.2 联调时常见的前后端对接问题
联调时最常遇到的坑,我按优先级列一下:
字段名不匹配。前端拿到的数据是null,十有八九是数据库字段下划线命名和Java驼峰属性没对映射上,或者实体类属性名和前端期望的参数名不一致。比如后端返回applyTime,前端写成了apply_time,自然取不到值。解决方法是前后端约定好字段命名风格,统一用驼峰,然后在后端VO里按前端期望的字段名组装。
日期格式问题。后端返回的日期默认是 Java 时间戳或yyyy-MM-dd HH:mm:ss格式,前端如果直接用会显示成一串数字或显示异常。我在后端配置了全局Jackson序列化规则,把LocalDateTime统一转为字符串形式,这样前端拿到就是标准格式,不需要额外写格式化函数。
图片访问405/404。上传图片后,前端无法访问图片URL,多半是静态资源映射没配置。我在项目里实现了一个WebMvcConfigurer,将本地磁盘的上传目录映射到/images/**虚拟路径,这样图片URL就可以直接用。如果是部署到服务器,同样的逻辑依然适用,只是路径换成服务器上的绝对路径。
6.3 联调阶段的调试方法论
联调不是简单的"前后端连上就完事",需要一套系统的调试方法。前端在Network面板查看接口返回状态码,如果看到401说明Token问题,看到400说明参数缺失或格式错误,看到500说明后端代码报错。后端则看控制台日志,MyBatis打印的SQL能帮助定位是查询条件出错还是数据本身有问题。
我的建议是后端开发时打开SQL日志,前端开发时保留Redux或Vuex的状态面板,这样两边各自定位自己的问题,出了事不会互相推诿。接口联调通过后再做功能走查,按用户的真实流程从注册→浏览→申请→审核→回访走一遍,每一步对比数据变化是否合理。
7. 构建、部署与一键环境搭建
7.1 后端Jar包构建与运行
后端使用Maven作为构建工具。在项目根目录执行:
mvn clean package -DskipTests构建成功后,target目录下会生成pet-adoption-0.0.1-SNAPSHOT.jar,这就是可以独立运行的完整应用。启动命令:
java -jar pet-adoption-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod生产环境的配置单独放在application-prod.yml,里面的数据库地址改成线上地址,文件上传路径改成服务器上的绝对路径。这样开发和生产的配置隔离,日常开发用dev配置,上线用prod配置,互不干扰。
7.2 前端构建与Nginx部署
前端构建命令:
npm install npm run build构建完成后,dist目录就是纯静态文件。我习惯用Nginx托管这些文件,并统一处理API反向代理:
server { listen 80; server_name pet.example.com; root /opt/pet-frontend/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }这里有几个容易踩的坑。第一,Vue Router如果开启history模式,刷新页面时会出现404,必须用上面的try_files指令把所有路由回退到index.html。第二,proxy_pass后面如果带/,分发的URL路径会去掉/api前缀,如果不带/则保留,得跟后端的context-path对应上。第三,上传的图片访问路径也要配置Nginx的location去映射磁盘目录。
7.3 Docker Compose一键编排
项目携带一份docker-compose.yml,用来编排MySQL和Java服务。如果你有Docker环境,一条命令就能把整套后端依赖启动好:
version: '3' services: mysql: image: mysql:5.7 container_name: pet-mysql environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: pet_adoption ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro backend: build: . container_name: pet-backend depends_on: - mysql ports: - "8081:8081"需要注意Docker容器里的MySQL初始化和MySQL版本兼容问题。5.7的认证方式和8.0有差异,如果容器起不来,先docker logs看MySQL日志,再检查初始化SQL脚本是否执行成功。这套编排方式适合本地快速搭建开发环境,也适合给需要演示的同学用,比自己一步步赖MySQL装服务省事多了。
8. 常见问题排查与避坑指南
8.1 环境问题速查表
我整理了这套项目在运行中最常遇到的10类问题,每一条都来自实际踩坑记录。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 后端启动报数据库连接失败 | MySQL服务未启动 / 密码错误 / 端口被占用 | 先确认MySQL进程存在,再用Navicat试连,最后检查yml配置 |
SQL注入异常:BadSqlGrammarException | 表名或字段名与保留字冲突 | 给表名字段名加反引号,或改名(比如desc、condition) |
| 中文乱码 | 数据库连接串缺编码参数 / 建库字符集不对 | URL加characterEncoding=utf8,建库用utf8mb4 |
| 上传图片返回403 | 拦截器拦了图片上传请求 | 在拦截器白名单里加上传接口,或放行OPTIONS预检请求 |
| 前端页面白屏 | JS报错 / 路由配置错误 / 构建产物路径问题 | F12看Console,先解决报错,再检查publicPath配置 |
| 接口返回401但已登录 | Token过期 / Header参数名不对 | 检查请求头是否带Authorization: Bearer xxx,重新登录 |
| 列表页分页数据不对 | limit/offset参数被前端传成字符串 | 后端Controller用@RequestParam接收并强转成int |
| MyBatis查询结果全是null | 驼峰映射没开启 | 配置map-underscore-to-camel-case: true |
| 跨域报错 | 前后端端口不同 | 开发环境配proxy,生产环境配Nginx反向代理 |
| 领养申请提交后宠物状态未变 | 事务未生效 / SQL条件没匹配 | 检查Service是否加@Transactional,看看宠物状态值是否匹配 |
8.2 业务逻辑与并发的问题
这类问题是"看起来能跑,细想却有大坑"的类型。最典型的就是同一只宠物被多人同时申请。如果没有状态原子更新,两个用户同时提交申请,后端先查宠物状态是AVAILABLE,然后A插入申请并更新状态,B也在同一时间插入申请并更新状态,最终两条申请都成功了,但宠物只有一只,逻辑上直接冲突。
解决这个问题的核心在SQL层级,不能用"先select再update"的乐观锁流程,而是用条件更新作为原子操作:
UPDATE pet SET status = 'APPLYING' WHERE id = #{petId} AND status = 'AVAILABLE'这条SQL能保证只有一只申请能成功修改状态,因为MySQL的行锁会串行化这个操作。影响行数为0说明宠物状态已经不是AVAILABLE,直接提示用户"宠物已被申请"。这是我对抗并发最常用也最稳妥的方式。在接口层面再把业务逻辑包在事务里,保证申请记录和宠物状态变更的原子性。
8.3 我会提前做好的几个优化预埋
实现这套系统时,我给后续扩展留了几个预埋点,大家拿到源码后可以继续做二次开发。
文件上传没有写在业务代码里,而是抽成了单独的FileController,后续接OSS只需要替换FileService的实现类,不用动Controller和前端逻辑。
宠物图片我设计的是逗号分隔存储多个URL,而不是只存一张。这样做的好处是宠物详情页天然支持多图展示,前端用split(',')拿到数组即可。
接口统一返回Result<T>结构体,包含code、message、data三个字段。如果想接入Swagger生成API文档,只需要加一个springfox依赖,并在Controller上标注解即可。后续前后端分离团队协作时,Swagger文档能省掉大量口头沟通成本。
9. 总结与后续扩展建议
9.1 这套源码的加分项与可以继续深挖的方向
从自己的实操经验出发,我觉得这套系统最值得借鉴的地方是它没有停留在简单的CRUD,而是把领养业务的核心流程做了完整闭环。状态机、事务控制、并发保护、角色权限这几个点,在答辩或面试中都能变成加分项。
如果要做二次开发,我最建议优先扩展的方向有三个。
第一是支付和费用功能,比如领养押金、绝育手术费认缴,这会引入微信支付/支付宝支付的接口对接,系统复杂度上一个台阶,但同时实用性也大幅提升。
第二是LBS功能,基于地图展示附近的领养点和宠物医院,帮助平台运营方对接线下资源。前端可以用地图SDK,后端存经纬度坐标并提供范围查询。
第三是消息推送和站内信。目前系统只在网页端展示审核结果,如果能加短信或微信模板消息推送,用户体验会好很多。这个功能在真实运营场景中几乎是刚需,否则用户得天天刷新页面才知道申请过没过。
9.2 以个人经验的视角给几句实在话
做了这么多年项目,我最大的感受是:源码本身只是起点,真正值钱的是你对业务逻辑的理解和对问题现场的排查能力。这套平台如果只是用来应付作业,跑通功能就够了;但如果你想从中真正学到东西,建议把每个Service方法都自己写一遍,把每条SQL都自己调通,把每个状态流转都画出来。
我当初在写这套系统时,光是领养审核的状态流转就和导师讨论了好几次,后来发现真实世界的领养流程远比我想象的复杂——有人会中途反悔,有人填的联系方式是假的,有人领养后失去联系。系统能做的只是用代码约束流程,真正的信任建立还是要靠线下。这也是我把回访设计成独立环节的原因。
最后分享一个小技巧:拿到任何系统源码,不要急着跑起来,先打开SQL脚本把表结构看一遍,再打开Controller看接口清单,然后打开前端路由看页面规划。这三样看完,你对整个系统的理解就已经超过一大半人了。之后再带着问题去读代码,效率会高得多。这套源码的内置注释也比较详细,跟着注释走,我相信每个人都能把它跑起来,并且跑得比自己预期更稳。