这套“Java SpringBoot+Vue3+MyBatis 同城上门喂遛宠物系统”最近在开发者圈子里讨论度不低。作为前后端分离的典型项目,它麻雀虽小五脏俱全,覆盖了从用户认证、订单流转到地图服务对接的完整链路。如果你是正在做毕业设计、想练手全栈项目、或者准备转Java开发岗的求职者,这套系统的源码拆解值得你花半小时读下去。它解决的不是“怎么敲代码”的问题,而是“一个真实的商业项目如何组织代码、设计表结构、安排业务流程”的问题。
1. 项目概述与功能全景
1.1 核心业务场景
先把这个系统到底做了什么说清楚。同城上门喂遛宠物,本质上是一个本地生活服务的撮合平台。宠物主人有出远门、加班、出差的需求,宠物没人管;另一边是闲在家里的宠物保姆——通常叫“宠物师”或者“遛宠师”,他们有时间、有经验、也愿意赚这份钱。平台要做的就是把这两类人匹配起来。
结合源码来看,这套系统的用户角色分得比较清晰:
| 角色 | 核心诉求 | 系统提供的核心功能 |
|---|---|---|
| 宠物主人 | 找人按时喂食、遛狗、陪玩 | 发布服务需求、下单、在线支付、实时查看服务进度 |
| 宠物师 | 接单赚钱、展示专业度 | 接单、展示服务项目、上传服务记录(照片/备注) |
| 管理员 | 保证平台有序运转 | 订单审核、用户管理、服务类目管理、数据统计 |
这里有个容易被忽略的点:很多类似系统会把“宠物师”做成一刀切的统一角色,但真实场景里宠物师也可能有专职和兼职之分,服务技能也分“只遛狗”“只喂猫”“上门照顾爬宠”等。我看了源码的表结构,它通过**服务类目表(service_category)**把服务类型做成了动态配置,这意味着运营方可以在后台随时增删服务项目,不需要改代码。这一点很加分,说明作者是有真实业务运营思维的。
1.2 整体架构设计
技术栈是标准的前后端分离:后端SpringBoot + MyBatis,前端Vue3 + Element Plus(或Vant,取决于用户端是移动端还是PC端),数据库MySQL。从前后端交互方式来看,是典型的RESTful API + JSON数据传输。
后端的包结构大致是:
com.petcare ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl // 业务实现 ├── mapper // MyBatis数据访问层 ├── entity // 数据库实体 ├── dto // 数据传输对象 ├── vo // 视图对象 ├── config // 配置类 ├── utils // 工具类 └── common // 通用返回、异常处理这种分层是Java后端最常见的规范:**Controller只做参数接收和结果封装,不写业务逻辑;Service层承载核心业务;Mapper层只负责和数据库打交道。**对于新手来说,最难理解的就是“为什么Controller不能直接写逻辑”。打个比方:把餐厅比作一个系统,Controller是大堂经理,Service是后厨,Mapper是仓库管理员。大堂经理可以收菜单(接收参数)、传菜(返回结果),但你不能让大堂经理钻进后厨去炒菜。一旦菜品出问题(业务异常),你总得知道是哪个环节出的问题——职责分明才好排查。
前端方面,这个项目用Vue3 + Vite构建,配合Vue Router做路由管理、Pinia做状态管理。看点在于:Composition API(组合式API)的用法。现在Vue3已经是主流,面试时几乎必问Composition API和Options API的区别。这套源码里对组合式API的使用可以作为参考范例——比如把“订单查询”相关的响应式状态和函数都封装在一个useOrderList()组合函数里,组件里只用调用这个函数拿数据,代码的复用性和可读性比Options API时代高了一大截。
2. 为什么选这四件套:技术选型的底层逻辑
2.1 前后端分离的价值
先说“前后端分离”这个概念。市面上很多学员项目号称前后端分离,实际上后端返回HTML页面,或者前端静态文件直接丢进后端项目里启动,这都不算真正的分离。
这套系统是真正的分离:**前端工程(Vue3项目)通过Vite启动在5173端口,后端SpringBoot启动在8080端口,前后端通过HTTP请求通信,部署时也是分开部署的。**为什么这么做?我根据自己的经验总结了三个核心原因:
第一,团队协作效率。前后端分离后,前端开发只需要根据后端给出的接口文档(Swagger或者YApi)来Mock数据,不必等后端代码写完才能开工;后端也只需要保证接口返回数据格式正确,不用关心前端长什么样。两边开发速度都明显加快。
第二,独立部署和扩展。系统访问量大了之后,前端静态资源可以扔到Nginx或者CDN上,减轻后端服务器压力;后端接口可以水平扩展,挂多台机器做负载均衡。如果前后端耦在一起,你只能整包扩展,资源浪费很严重。
第三,多端适配。现在做业务系统,往往不只有一个PC后台,还要有小程序、App、H5。后端接口做一次,所有端都能共用同一套数据接口。这套系统里用户端和后台管理系统可能是两个不同的前端工程,但共用同一个后端,这就是前后端分离的最大价值。
2.2 SpringBoot + MyBatis:为什么是这个组合
SpringBoot在Java后端中的地位不用多说。它的核心优势是“自动配置”和“约定优于配置”。以前用SSM(Spring+SpringMVC+MyBatis)搭建项目,光是配置就要写一堆XML文件,而且每个项目配置基本一样,繁琐又容易出错。SpringBoot把这些固定动作做成自动配置——你引入一个spring-boot-starter-web依赖,内置的Tomcat直接就启动了,一个@RestController就能开始写接口。这在项目开发中节省了大量时间,也让Java后端开发的门槛大幅下降。
再说到MyBatis。为什么不用Spring Data JPA?
这里有一个很关键的区别必须讲清楚:**JPA是面向对象的思维,MyBatis是面向SQL的思维。**如果业务表格很简单、关联关系少,JPA用起来特别舒服,因为框架帮你自动生成SQL。但这种项目的订单查询往往涉及多表关联——订单表、用户表、宠物表、服务类目表、地址表,查询条件还动态变化(按城市、按服务类型、按订单状态),用JPA去拼接动态SQL会很难受。MyBatis原生支持动态SQL标签(<if>、<where>、<foreach>),写复杂查询时直截了当,SQL是什么样一眼就能看清,性能调优也方便。
另外,MyBatis还有个容易被新手忽略的优势——XML和注解两种方式并存。简单SQL用注解直接写在Mapper接口上;复杂SQL写在XML里,与Java代码解耦。这套系统里的订单列表查询接口就是典型的复杂SQL,用了<where>标签和<if>标签做动态条件拼接,如果你打开源码里的OrderMapper.xml看一眼,马上能理解那种“一张表一个mapper,按需拼接SQL”的整洁感。
2.3 Vue3:为什么不用Vue2
Vue3首发的版本是2020年9月,到现在已经有大量生产环境验证。对于新项目来说,用Vue2的理由已经越来越少。Vue3的Composition API解决了Vue2中Mixins带来的命名冲突和逻辑不清晰问题;用ref和reactive处理响应式数据比Vue2的data对象灵活得多;Vite的冷启动速度比Webpack快了一个数量级——一个中大型前端项目,Webpack冷启动可能要30秒往上,Vite基本3秒以内。
这套系统用到的Vue3技术点值得照着敲一遍:
- 组合式API:
<script setup>语法糖,省去一堆export default代码 - 响应式API:
ref处理基础类型和对象,reactive处理深层次对象,computed处理派生状态 - 路由守卫:因为系统分“用户端”和“管理端”,需要根据登录状态和角色做路由拦截——没登录跳登录页,角色不对跳403页
- Pinia状态管理:存储用户token、用户信息和角色信息,刷新页面后数据不丢
其中我最推荐学习的是组合式函数的封装方式。源码里通常会有一个src/composables/目录,里面放着类似useOrderList、useUserInfo这样的函数。组件里只需要:
const { orderList, loading, fetchOrders } = useOrderList() fetchOrders()一行代码就把订单列表的数据加载、加载状态、错误处理都从组件里抽出去了。这种写法在团队规模扩大之后尤其重要——因为它强迫你把逻辑和UI解耦,逻辑可以独立测试,UI组件可以保持纯粹。
2.4 MySQL:数据如何持久化
MySQL至今仍是中小型业务系统的首选数据库,原因很简单:成熟稳定、生态完善、团队招人容易。这套系统的表结构大概包含这些核心表:
user—— 用户表(含宠物主和宠物师,用role字段区分)pet—— 宠物档案表service_order—— 服务订单表service_category—— 服务类目表address—— 地址表payment—— 支付记录表order_comment—— 订单评价表
从表的命名可以看出来,作者遵循了基本的数据库设计规范:**单词小写、下划线分隔、主键用id、外键用xxx_id结尾、所有表都带上create_time和update_time字段。**这种规范在真实公司里几乎是强制要求,因为维护成本低,老程序员接手新代码时能一眼看懂。
这里我必须提醒一句:**设计数据库表时,时间字段类型优先选择datetime,不要用timestamp。**虽然timestamp在2038年之前够用,而且能自动转换时区,但如果你将来做多地域部署,时区转换的坑能烦死你。datetime存的就是一个字面值,备注里写明“北京时间”,永远没有问题。
3. 数据库设计与核心表结构拆解
3.1 用户与宠物模块设计思路
用户表是整个系统的地基。它里面有一个role字段,值是1代表宠物主人,2是宠物师。有些同学喜欢拆两张表,一张pet_owner一张pet_sitter,这样看起来“更规范化”,实际使用中发现两个角色的公共字段重复度极高,而且将来如果出现“同一个用户既是宠物主又是宠物师”的场景,拆表就会非常尴尬——你得在两套表里都插入一条数据,还要处理同步问题。用一个通用user表加role字段,一个用户的多个身份切换起来非常顺滑。
宠物表的设计要点在于“建档”和“过敏信息”的联动。宠物主注册完账号后,需要给每一只宠物建立档案,包括昵称、品种、年龄、性别、体重、是否绝育、有没有攻击性、饮食禁忌等信息。这些字段看似琐碎,但其实直接影响订单匹配的效率——比如有的宠物师不接大型犬,如果订单页面可以直接看到宠物品种和体重,宠物师就能快速判断接不接单,省去反复沟通的麻烦。
3.2 订单表:业务状态流转的枢纽
订单表是整个系统最核心的表,我把它单独拿出来讲。它的字段大致包括:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单编号(业务唯一) |
| owner_id | bigint | 宠物主人ID |
| sitter_id | bigint | 宠物师ID |
| pet_id | bigint | 宠物ID |
| service_type_id | bigint | 服务类目ID |
| status | tinyint | 订单状态 |
| service_time | datetime | 预计上门时间 |
| service_duration | int | 服务时长(分钟) |
| address_id | bigint | 服务地址ID |
| total_amount | decimal(10,2) | 订单总金额 |
| remark | varchar(500) | 备注 |
| create_time / update_time | datetime | 创建/更新时间 |
重点说订单状态。这里用tinyint存状态码,而不是直接存“待接单”“进行中”这种中文,原因有两个:一是数字占空间小、查询速度快;二是状态码和状态描述分离,将来修改状态文案不需要动数据库。整套系统的订单状态流转是:
待付款 → 待接单 → 已接单 → 服务中 → 已完成 → 已评价 ↘ 已取消状态流转的实现方式是严格的:每个状态变化都通过Service层的业务方法完成,并且方法开头会校验当前状态是否允许跳转到目标状态。比如“已取消”订单不允许再变成“已接单”,“已完成”订单不允许再变回“服务中”。这种状态机校验写起来不复杂,但能挡住很多脏数据。
这里有一个非常实用的经验:**订单编号生成不要用数据库自增ID,要用“日期+随机数+用户ID尾号”这种业务可读编号。**自增ID直接暴露在前端,不仅让竞争对手能预估你的业务量,还会被有心人用来遍历订单数据。这套系统的order_no大概是20250101120000123这种格式,前段是日期时间,中间几位是随机序列,尾部是对应用户ID的后几位。这样即使不关联查询,运营人员在后台看到订单号也能知道大概的生成时间和用户特征。
3.3 为什么添加“服务地址”单独表
地址可能是一张容易被忽略的表,但在这类同城服务系统里,地址不仅承载“门牌号”信息,更是整个推荐匹配策略的数据基础。地址表里至少要有省市区编码、详细地址、经度、纬度四个核心字段。
经纬度字段很关键。宠物师接单的时候,系统需要计算宠物师所在位置和宠物主家的距离,超过一定范围(比如10公里)就直接不展示订单,因为宠物师不可能为了遛一次狗跨半个城市跑一趟。距离计算通常有两种方案:
第一种是MySQL直接算,用ST_Distance_Sphere函数(MySQL 5.7及以上支持),一条SQL就能按距离排序。
第二种是应用层算好再查询,先用经纬度算出距离,再作为参数传入查询。
源码里大概率是第一种方案,因为实现简单、数据不需要额外同步。但你要是做的是高并发系统,建议把经纬度改成Redis GEO结构,配合地理位置索引,性能会好很多。对这个系统来说,MySQL自带的空间函数完全够用,不必为了炫技而引入新组件。
4. 后端核心业务功能与接口实现
4.1 用户认证与授权:JWT怎么做
系统的认证方式用的是JWT(JSON Web Token)。刚学后端的人容易把“登录”想成只有“校验用户名密码”一步,其实完整的流程是:
- 用户提交手机号+验证码(或密码)到后端
- 后端校验通过后,生成一个JWT,返回给前端
- 前端把JWT存在
localStorage里,每次请求在Authorization头带上 - 后端通过拦截器解析JWT,确认用户身份和角色
- 对于管理端接口,还会再校验一次角色是否匹配(比如只有
role=0的管理员才能访问用户管理接口)
JWT的三个部分:Header、Payload、Signature,前两部分是Base64编码,可以直接解码看到内容;最后一部分是签名,用于防止内容被篡改。我在这个项目里特别想提醒的是:不要把敏感信息直接放Payload里。比如用户手机号,放了就能看到;密码更不能放,因为就算有签名,Payload本身是明文可见的。正确的做法是只放userId和role,其他信息每次请求时再查数据库。虽然多一次查询,但安全性和系统扩展性都好得多。
另一个容易被忽略的安全点是密码存储。这个系统里用户表存的是bcrypt加密后的密码哈希。为什么不用MD5?因为MD5是“摘要函数”,设计目标就是快,你算得快攻击者也算得快,彩虹表一查就出结果。BCrypt算法内置随机salt,同一个密码每次加密结果都不同,且刻意设计得比较慢,暴力破解成本高得多。
4.2 订单流程:从下单到完单的完整闭环
订单模块是整个系统的业务核心。从源码里看,一个下单请求从前端发起后,后端的处理逻辑大致是:
**第一步:参数校验。**前端可能因为bug或者用户反复点击,发送了非法数据,后端必须校验。比如服务时间不能是过去时间,宠物ID必须属于当前登录用户,订单金额不能是负数。
**第二步:扣减库存/校验状态。**这里库存指的不是实物库存,而是宠物师的“可接单时间”。如果宠物师在某个时间段已被其他订单排满,新的订单就不能再接。实现方式上,源码使用了悲观锁或者乐观锁——最常见的做法是在下单时先SELECT ... FOR UPDATE锁定宠物师的排班记录,确认无冲突后插入新订单。
**第三步:生成订单编号并保存。**这个上面已经说过了,编号生成用业务规则,不依赖自增。
**第四步:调用支付接口。**如果是对接真实支付,这里要调用微信支付/支付宝预下单接口,把支付参数返回前端,前端拉起支付组件。如果是演示项目,通常用一个“模拟支付”接口直接置为已支付。
**第五步:发送通知。**下单成功后,需要通知宠物师有新的可接订单。源码里如果是简化的实现,可能是轮询订单列表;如果做了消息推送,会有WebSocket或者第三方推送(类似极光推送)的接入。这里牵扯到一个现实问题:宠物师端必须实时感知新订单,实现方式的选择影响很大。轮询的问题在于延迟和无效请求多;WebSocket适合即时通讯场景,但宠物师如果App不在前台,连接会断开,这时候推送也需要兜底方案。考虑到这套系统的定位,源码中大概率用了轮询+页面刷新提醒,这也是可接受的简化方案。
再往下走,宠物师接单这个动作同样有并发风险:多个宠物师同时抢同一个订单怎么办?正确做法是在更新订单状态时加上WHERE sitter_id IS NULL这个条件。如果宠物师A先提交,更新成功但影响行数为1;宠物师B再提交,sitter_id已经有值了,更新影响行数为0,响应“订单已被他人接走”。这是最简单的乐观锁实现,不需要额外引入Redis分布式锁,性能也够用。
接单后就是服务履约阶段。宠物师上门后,系统提供一个“开始服务”按钮,点击后状态变为“服务中”;服务结束时点击“完成服务”,订单状态变成“待评价”。这个过程看似简单,但真实运营中还有个更细的需求:宠物师在服务过程中可以上传服务照片、备注喂食情况、甚至发一段短视频。这些内容要存到订单明细表,将来售后有争议时调取证据链。这套系统的表结构里如果没做“服务记录表”,那至少有一个“订单追踪表”,保存状态变更的时间和操作人ID。
4.3 MyBatis动态SQL实战:订单查询怎么写
订单列表页是用户端和管理端都会用到的功能,但查询条件不一样。用户端可能是“只看我发布的订单”,管理端则是“按城市/按状态/按宠物师名称筛选”。如果每个查询条件都写一个不同的接口和Mapper方法,那代码冗余度会非常高。
MyBatis的动态SQL就是为这种场景设计的。源码里OrderMapper.xml通常长这样:
<select id="selectOrderList" resultType="com.petcare.vo.OrderVO"> SELECT o.*, u.nickname AS owner_name, p.name AS pet_name, c.name AS service_name FROM service_order o LEFT JOIN user u ON o.owner_id = u.id LEFT JOIN pet p ON o.pet_id = p.id LEFT JOIN service_category c ON o.service_type_id = c.id <where> <if test="ownerId != null"> AND o.owner_id = #{ownerId} </if> <if test="sitterId != null"> AND o.sitter_id = #{sitterId} </if> <if test="status != null"> AND o.status = #{status} </if> <if test="cityCode != null and cityCode != ''"> AND o.address_id IN ( SELECT id FROM address WHERE city_code = #{cityCode} ) </if> </where> ORDER BY o.create_time DESC LIMIT #{offset}, #{pageSize} </select>这个SQL的设计有三个值得学习的点:
第一,<where>标签自动处理条件拼接。第一个条件前面不用写WHERE也不会报错,因为MyBatis会自动判断有没有条件语句,没条件时整个WHERE都不会拼进去。
第二,LEFT JOIN带出关联表信息。订单列表页要显示宠物名和主人昵称,这是典型的多表关联场景。如果你把所有信息都冗余到订单表里,维护成本会高;如果只在列表页做二次查询,又会造成N+1问题。LEFT JOIN一次查出结果是常规做法,也是最优解。
第三,分页用LIMIT offset, pageSize。这种物理分页对MySQL来说效率尚可。有人会说应该用LIMIT #{pageSize} OFFSET #{offset},这是PostgreSQL的语法,MySQL里必须用LIMIT offset, pageSize的格式,别搞混了。
4.4 前后端交互中的VO与DTO区别
刚接触企业级项目的同学经常把entity、VO、DTO混为一谈。在这套系统里它们的职责其实是比较清晰的:
entity:和数据库表字段一 一对应的实体类DTO:接收前端请求数据的对象(比如登录请求DTO、下单DTO)VO:返回给前端展示用的对象(比如用户登录后返回的用户信息VO)
为什么要区分?因为三层之间的数据“长得不一样”。比如新增订单时,前端传来的参数是petId、serviceTypeId、serviceTime、addressId、remark,这些和service_order表的字段能对上,可以用entity直接接收。但返回订单列表时,前端要的不只是订单表的字段,还有宠物名称、宠物师昵称、服务类目名称,如果还用entity返回,前端拿不到这些关联信息;如果硬把entity加上这些字段,那entity和表结构就不对等了。
**规范的措辞是:数据库表结构变动导致的entity改动,不应该影响到接口的出参格式。**所以你会看到源码里controller返回类型几乎都是Result<T>,其中T是VO对象。这种封装带来的好处是:即使未来把订单表的字段改了,只要VO的字段没变,前端代码一行都不用动。
5. 前端核心页面与功能实现
5.1 用户端:移动优先的体验设计
用户端是给宠物主人用的,场景决定了它必须针对手机屏幕做适配。Vue3 + Vant(移动端组件库)或者Vue3 + Element Plus(PC适配)取决于作者的目标——看标题是“同城上门”,大概率会偏向移动端布局,也就是手机上能直接在微信里打开H5页面完成下单。
用户端的核心页面大概有这四类:
首页(服务列表页):顶部是城市定位,下面是服务类目的卡片,比如“上门喂猫”“上门遛狗”“宠物陪伴”。每一个类目卡片都有图标、标题、价格和简介。点击进去,是详细的服务说明页,底部一个大大的“预约”按钮。
下单页:选择宠物、选择服务时间、填写地址、补充备注。这里有个细节:用户下单时如果宠物档案还没建,系统会先引导去建档案,而不是直接报错。这种“前置条件未满足时引导用户解决”的产品思维,值得前端开发学习——不是所有交互都靠弹窗报错,有时候一个温柔的引导入口就能提升不少转化率。
订单列表页:用户能看到所有历史订单,按状态分成“待接单”“服务中”“已完成”等标签页。状态变化时,列表页要能实时刷新。如果用了WebSocket或者轮询,前端轮询的时间间隔要注意——5秒一次比较合适,太频繁浪费后端资源,太慢体验又跟不上去。
个人中心:基本信息、宠物档案管理、地址管理、退换货售后、客服入口等。
从Vue3的实现角度看,这套系统前端的亮点在于状态管理是严谨的。用户token存在Pinia里,每次请求通过axios拦截器自动加上Authorization头;如果接口返回401(token失效或过期),前端自动清空用户信息并跳转登录页。这个“统一处理401”的逻辑,是很多半吊子项目都漏掉的地方——他们只会在单个请求里写catch,token过期后用户看到一个红红的报错,而不是被温柔地带回登录页。
5.2 宠物师端:接单与履约效率
宠物师端的页面逻辑相对简单,但操作频率更高。核心是订单列表和订单详情。
订单列表默认按照距离和发布时间排序,宠物师能看到“单主宠物类型”“服务时间”“预计收入”。接单按钮在列表页直接露出,是出于效率考虑——宠物师刷单就像外卖骑手刷单一样,看中的就是信息一目了然、操作一步到位。
订单详情页包含宠物档案、服务地址、主人备注,还有联系主人的入口(通常是虚拟号码拨号)。服务开始后,宠物师需要上传服务照片和填写喂食记录。这部分做得好不好,直接决定了平台对宠物主的可信度。我见过不少系统在“服务记录”这块做得很粗糙,比如只允许传一张照片、备注字数上限太短等,其实需求方很在意“你去喂了没有,猫的状态怎么样”,多照片+文字描述是底线要求。
5.3 管理后台:Vue3 + Element Plus的数据看板
管理后台用Vue3 + Element Plus是最成熟的组合。页面一般包括:
- 仪表盘:今日订单量、营收总额、新增用户数、服务完成率
- 用户管理:用户列表、状态禁用/解禁
- 宠物师管理:资质审核(身份证、健康证)、接单量统计
- 订单管理:条件查询、异常订单处理(取消、退款)
- 服务类目管理:增加/编辑/下架服务类目
管理后台的前端技术点集中在表格组件和表单校验上。Element Plus的el-table配合自定义列渲染基本能覆盖所有列表需求;表单用el-form的rules配置校验规则。这一块对前端求职者来说是比较容易上手的方向,照着源码写一遍,基本就把后台管理系统的常见套路摸清了。
6. 部署上线与常见问题排查实录
6.1 本地跑通这套系统的完整流程
拿到源码之后,很多人卡在第一步——跑不起来。我把自己踩过的坑整理成一份“一步一图”的启动清单,你可以直接照着做。
后端启动步骤:
- 安装JDK 8或11,同时配置
JAVA_HOME环境变量。命令行输入java -version验证,版本没问题再继续。 - 安装MySQL 5.7+,创建数据库,比如
pet_care_db,字符集一定要选utf8mb4。字符集是很多人忽略的坑:如果你用默认的latin1,存中文数据的时候会变成乱码。utf8mb4兼容utf8且支持emoji,是MySQL 5.7以上的标准选择。 - 执行源码中提供的
sql/init.sql脚本,导入表结构和初始数据。注意脚本里可能有多个用户角色的初始账号,记录下来方便测试。 - 打开
application.yml(或application-dev.yml),把数据库连接信息改成你自己的:
spring: datasource: url: jdbc:mysql://localhost:3306/pet_care_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver这里特别提醒:serverTimezone=Asia/Shanghai一定要配,否则会报时区错误——MySQL 8.0默认时区和本地不一样,Java连接时会报The server time zone value '�й���ʱ��' is unrecognized。 5. 在项目根目录执行mvn spring-boot:run,看到Started Application日志就说明启动成功。
前端启动步骤:
- 安装Node.js 16+,然后再全局安装pnpm或直接用npm。
- 在
frontend目录执行npm install。如果安装慢,可以把镜像源换成国内源:npm config set registry https://registry.npmmirror.com。 - 执行
npm run dev启动开发服务器,浏览器访问http://localhost:5173。 - 如果后端接口报跨域错误,检查项目里是否配置了跨域处理。SpringBoot的常见做法是加一个
CorsConfig配置类,允许http://localhost:5173的跨域请求。
注意:如果你把前端部署到Nginx,而接口还是走后端8080端口,需要配置Nginx反向代理,把
/api的请求转发到后端。这一步是前后端分离部署的必修课,不建议跳过。
6.2 开发中踩过的坑:高频问题速查表
我把这个项目开发过程中常见的问题整理成一张表,每一条都是我自己实操中遇到过并排查解决的:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 请求接口报404 | 前端请求路径和后端@RequestMapping不一致 | 检查后端接口路径,特别是带参数拼接的/user/{id}格式 |
| 接口报401 | JWT过期或token未携带 | 前端检查axios拦截器是否设置了Authorization头;检查token是否过期 |
| 中文乱码 | 数据库字符集不是utf8mb4 | 统一数据库、连接串、页面编码均为utf8mb4 |
| 跨域报错 | 前后端端口不一致,未配置CORS | 在后端加CorsConfig过滤器,允许前端源 |
| 数据库连接失败 | 驱动版本不匹配 | SpringBoot 2.x用com.mysql.cj.jdbc.Driver,MySQL 5.7和8.0均可 |
| 页面加载白屏 | 前端路由或静态资源路径错误 | 检查vite.config的base配置,部署到子目录时需要设置base: './' |
| 订单列表数据不对 | 动态SQL拼接条件写错 | 打开MyBatis SQL日志,查看实际执行的SQL |
其中最后一个问题值得展开说。调试MyBatis动态SQL最直接的手段就是打开SQL日志。在application.yml里加一行:
logging: level: com.petcare.mapper: debug这样控制台会把每个Mapper接口实际执行的SQL语句打印出来,?占位符对应的参数也会一起打印。看到实际SQL,你就知道问题是SQL本身写错了,还是参数传错了。
6.3 上线部署选什么
这套系统的部署方案,如果只有一台2核4G的云服务器,最省资源的方案是:
- 前端构建完静态文件,丢给Nginx托管
- 后端打jar包,用systemd服务保持后台运行
- MySQL也装在同一台机器上,数据库定期备份
Nginx配置的核心片段:
server { listen 80; server_name 你的域名; location / { root /var/www/pet-front/dist; index index.html; try_files $uri $uri/ /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; } }这里try_files $uri $uri/ /index.html;这行很重要——Vue Router如果用的是history模式,刷新非首页路由时Nginx会直接404,必须通过try_files把请求重定向到index.html,交给前端路由接管。
后端jar包启动建议用systemd管理,而不是直接nohup java -jar。systemd的好处是:开机自启、崩溃自动重启、日志集中管理。比较简单的服务配置是这样:
[Unit] Description=Pet Care Backend After=mysql.service [Service] ExecStart=/usr/bin/java -jar /opt/pet-care/app.jar Restart=always RestartSec=10 User=root [Install] WantedBy=multi-user.target把文件放到/etc/systemd/system/pet-care.service,然后执行systemctl daemon-reload && systemctl enable --now pet-care就完成了。
7. MyBatis源码级调优:从“能用”到“好用”
7.1 一级缓存与二级缓存
面试高频题“MyBatis缓存”在这套系统中也值得实践一下。MyBatis默认开启一级缓存(SqlSession级别),同一个SqlSession内执行相同SQL会直接返回缓存结果,不查数据库。问题在于SpringBoot中每个Service方法通常对应一个SqlSession,方法结束SqlSession就关闭了,一级缓存在Spring环境中作用不大。
二级缓存是Mapper级别的,跨SqlSession共享。但这套系统不建议开,理由是:二级缓存需要每个实体类实现Serializable接口,并且对热点数据的实时性要求很高的场景(比如订单状态),缓存了反而容易出问题——状态变了,缓存还没刷新,用户看到的是旧数据。这种项目对数据实时性要求高,不如把缓存彻底关掉,保证查询结果永远是数据库最新状态。
7.2 分页插件的引入
MyBatis自带的分页逻辑是LIMIT #{offset}, #{pageSize},听起来简单,但问题在于:如果你写复杂的动态SQL,LIMIT位置稍微写错就会报错。这时候引入PageHelper插件是最省事的方案。它通过拦截器自动在SQL后面追加LIMIT语句,并且自动生成SELECT COUNT(*)查询总条数。用法很简单:
PageHelper.startPage(pageNum, pageSize); List<OrderVO> list = orderMapper.selectOrderList(query); PageInfo<OrderVO> pageInfo = new PageInfo<>(list);但这里有个坑:**PageHelper是线程安全的吗?**它的startPage方法使用了ThreadLocal存储分页参数,如果后续有多个查询方法,第二个查询会误以为也要分页。所以正确的用法是分页参数生效的那个查询方法后面,必须紧跟这段查询代码,中间不要插入其他Mapper方法。这个Peter的经验在面试中如果你能说出来,绝对是个加分项。
7.3 SQL性能排查工具
生产环境如果你不确定某条SQL执行得慢不慢,打开MySQL慢查询日志是排查利器。MySQL 5.7及以上配置方式:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';这样执行时间超过1秒的SQL都会被记录下来。结合EXPLAIN语句分析有没有走索引、扫描行数多少、是否产生了临时表,基本能把慢查询问题都定位出来。
这套系统里最可能出现性能瓶颈的两个场景:
一个是订单列表的LEFT JOIN+子查询。如果用户表数据超过100万条,LEFT JOIN会把三张表的所有数据都取出来做关联,非常慢。优化思路是:先在大表上过滤条件(比如只查近30天订单),再JOIN小表。
另一个是距离排序查询。如果用ST_Distance_Sphere,它会扫描全表计算距离,不走索引。这时需要把经纬度范围缩小:先按经纬度粗筛出一个矩形范围(比如经纬度上下浮动0.1度),再在这个结果集里计算精确距离。这个优化能把查询范围从全表缩小到城市内的局部区域,性能提升非常明显。
8. 这套源码能学到什么:我的横向总结
如果你认真读完了上面每一段,你会发现这套“同城上门喂遛宠物系统”核心技术点并不神秘,但链条很完整——从前端交互到后端接口,从数据库设计到部署上线,每一步都有真实的工程决策痕迹。
我最大的感受是:**它不是一个“为了教学而教学”的玩具项目,它的每个功能点都能对应到真实业务场景中的某个痛点。**比如宠物师接单的并发控制、订单状态机的严格流转、按距离匹配宠物师、前端401统一处理,这些都是工作中天天会碰到的问题,只是在培训教程里大多数都一笔带过了。
如果你正在准备Java开发岗面试,我强烈建议把这只项目吃透。面试官问项目经验时,你可以不谈“我写了一个宠物平台”,而是谈“我设计了一套订单状态机,能自动拦截非法状态流转”“我用乐观锁实现了宠物师并发抢单的零超卖”“我把查询接口的VO和Entity分离,避免表结构变动引发接口变更”——这些话术背后的设计思想,一次性把简历上的项目亮点展现得清清楚楚。
最后分享一条个人经验:**不要停留在把系统的代码跑起来、页面能点动的阶段。**把源码里的每一个“为什么”问一遍——为什么订单状态用数字不用枚举字符串,为什么用Composition API而不用Mixins,为什么连表查询选择LEFT JOIN不用IN子查询——当你真的能把这些问题在自己的语言体系里重新讲一遍时,而不是背词条,这套源码才算真正属于你了。