宠物经济这两年有多热,不用我再多渲染,大家身边一定有那种“出差三天,猫主子没人喂”的朋友。同城上门喂遛宠物这个需求,早就从零散的兼职帖子,变成了不少团队认真做的本地生活服务项目。我最近用 SpringBoot + Vue + MyBatis + MySQL 这套经典组合,完整做了一个前后端分离的同城上门喂遛宠物系统,从数据库设计到接口开发,再到前端页面联调和 Linux 服务器部署,整条链路都跑通了。这篇文章把我从 0 到 1 的完整思路、核心模块实现方案、以及部署时踩过的坑全部梳理出来,给正在做类似项目或者准备入坑前后端分离实战的朋友一个可以直接参考的样本。
这个系统解决的核心问题很简单:宠物主人没时间或者不在本地时,可以在平台上发布喂食、遛狗、陪玩等需求,审核通过的服务人员(遛宠师)接单,上门服务完成后订单结算,双方互相评价。平台作为中间方,负责订单流转、资金记录、信用沉淀。业务场景不复杂,但麻雀虽小五脏俱全,用户体系、订单状态机、支付流转、评价体系、消息通知、定时任务一个不少,非常适合用来练手前后端分离项目,也适合作为毕业设计或者个人作品集项目。
1. 项目定位与业务闭环拆解
1.1 为什么选“上门喂遛宠物”这个场景
我在选项目场景的时候其实对比过好几个方向:二手交易、家政保洁、跑腿代办,最后还是选了宠物上门服务。原因是这个场景的业务边界足够清晰,但又不至于简单到没有技术含量。
从需求侧看,宠物主的核心诉求就三个:有人按时上门、服务过程可监督、结束后有反馈。从供给侧看,遛宠师关心的是订单是否充足、价格是否合理、收入是否透明。从平台侧看,需要解决的是信任问题——怎么让双方愿意通过平台完成交易。这三个角色的诉求一叠加,系统的功能模块就自然浮现了:用户管理、宠物档案、服务项目管理、订单中心、支付记录、评价体系、消息通知。每个模块都有明确的业务含义,不像那种为了凑页面而硬造出来的项目,逻辑是自洽的。
这个场景的另一个好处是,它的订单流转天然适合用状态机来表达。待接单、已接单、服务中、待确认、已完成、已取消,每个状态都有明确的触发条件和操作方,这比那些纯 CRUD 的练习项目有深度得多,在面试或者答辩的时候也更容易讲出亮点。
1.2 系统的角色划分与核心流程
整个系统我设计成三角色模型:
- 宠物主用户:注册登录后添加宠物档案,发布服务需求,查看订单进度,确认服务完成,发起评价。
- 遛宠师(服务人员):提交入驻申请,管理员审核通过后可以接单,上门服务后更新订单状态,查看自己的服务记录和收入。
- 平台管理员:审核遛宠师入驻资质,管理服务项目分类,处理异常订单和用户投诉,查看平台运营数据。
核心业务流程走一遍大概是这样的:宠物主下单选择服务项目(比如上门喂猫、遛狗一小时)并填写宠物信息和服务地址,系统根据服务范围和遛宠师的在线状态进行订单推送,遛宠师接单后按照约定时间上门,服务完成后上传现场照片并标记完成,宠物主确认无误后订单完结,平台完成资金结算记录,双方互评。
这个流程做下来,前端的页面结构也清晰了:用户端有首页、服务列表、下单页、订单列表、订单详情、个人中心、宠物管理;遛宠师端有接单大厅、我的订单、收入统计、入驻申请;管理后台有用户管理、审核管理、订单管理、内容管理。三大端口的页面一铺开,一个完整项目的体量感就出来了。
1.3 这个项目适合谁、能学到什么
如果你是刚学完 SpringBoot 和 Vue 基础、想找一个完整项目练手的人,这个项目能把前后端分离开发的完整流程串起来:从需求分析到接口设计,从数据库建模到前端页面联调,从本地调试到服务器部署。每一个环节都是工作中真实会遇到的。
如果你是在准备毕业设计或者求职作品集,这个项目的含金量在于业务闭环完整、技术栈主流、而且有实际部署经验可以讲。面试官问到“你的项目怎么处理的订单并发”“接口鉴权怎么做”“跨域问题怎么解决”,你都有真实的实践经历可以回答,而不是背概念。
如果你是想接私活或者做外包的开发者,这套系统的业务模型可以直接复用,换一个行业场景(比如上门保洁、同城跑腿、上门家教),核心的订单流转和用户体系基本不用大改。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么是这套组合
2.1 前后端分离架构的优势与适用性
这个项目我毫不犹豫选了前后端分离架构,前后端通过 JSON 格式的接口数据交互,各自独立开发、独立部署。
前端只负责页面渲染和用户交互,通过 axios 调用后端接口获取数据,完全不用关心服务端是怎么实现的。后端只负责业务逻辑和数据处理,通过 SpringBoot 提供 RESTful API。这么做最大的好处是团队协作时可以并行开发,前端不用等后端写完接口才能动工,后端也不用被前端的页面细节绑架,只要把接口契约定义清楚就行。
对于这种业务规模的项目来说,前后端分离还有一个隐性好处:后端服务可以被多个客户端复用。同一个后端接口,既能支撑 Web 管理后台,也能支撑将来的微信小程序、App、H5 等多端应用。我在设计接口的时候就刻意保持了接口的无状态性(通过 JWT 令牌鉴权),这样以后扩展终端非常容易。
2.2 SpringBoot 为什么够用且好用
后端我用的是 SpringBoot 2.7.x,没有赶时髦上 Spring Cloud 那套微服务全家桶。原因很简单:这个项目的业务规模用单体应用绰绰有余,引入微服务只会增加部署和运维的复杂度,对项目本身没有任何实际收益。
SpringBoot 的核心价值在于自动配置。内置的 Tomcat 容器让应用可以直接通过 java -jar 启动,不需要单独装 Web 服务器;Spring MVC 的注解开发方式让接口书写变得非常简洁;Spring 的依赖注入机制让代码的解耦程度很高,后续扩展新功能只需要加新的类,几乎不需要改动已有的代码结构。
具体到我们这个项目,SpringBoot 的 Starter 机制省了很多事。spring-boot-starter-web 提供了完整的 Web 开发能力,spring-boot-starter-validation 解决参数校验,spring-boot-starter-quartz 或者直接用 @Scheduled 注解处理定时任务,Spring 自带的事务管理机制保证订单操作的数据一致性。
2.3 Vue 版本选择与分析
前端框架我用的是 Vue 2.7 + Element UI。可能有朋友会问,现在 Vue 3 都出来这么久了,为什么还用 Vue 2?
最核心的原因是生态稳定性和团队熟悉度。Element UI 对标 Vue 2 的组件库已经非常成熟,各种表格、表单、弹窗、分页组件开箱即用,开发效率极高。很多企业项目到现在依然在用 Vue 2 维护,Vue 2.7 是官方最后一个版本,也 backport 了 Composition API 的部分特性,日常开发完全够用。
如果你是自己学习用,直接上 Vue 3 + Vite + Element Plus 也没问题,思路和实现方式区别不大。我这套项目的代码逻辑是很清楚的,前端页面只是数据的呈现和交互,你把 Element UI 的标签替换成 Element Plus,再调整一下引入方式,迁移成本很低。核心的 vue-router 路由配置、Vuex/Pinia 状态管理、axios 封装,在 Vue 2 和 Vue 3 里的思路是完全一致的。
2.4 MyBatis + MySQL 在中小型项目中的性价比
数据库持久层我选的是 MyBatis,数据库用的是 MySQL 8.0。
选 MyBatis 的原因是它对 SQL 的控制力最强。对于这种业务逻辑比较复杂的订单系统,多表关联查询、动态条件拼接、状态统计是家常便饭。MyBatis 允许我直接编写 SQL 语句,配合 XML 文件里动态 SQL 的 if、where、set 标签,能非常灵活地处理各种查询场景,不会像 JPA 那样在复杂查询时出现性能不可控或者 SQL 难优化的情况。
MySQL 8.0 的选择没什么可说的,开源数据库里综合实力最均衡的选手。窗口函数、CTE 公共表达式、JSON 类型支持都比 5.7 强了很多。需要注意的一点是,8.0 的认证插件默认是 caching_sha2_password,如果你的数据库连接工具或者 JDBC 驱动版本比较旧,会出现连不上的情况。我本地用的 MySQL 8.0.33,JDBC 驱动用 8.0.33 版本配套,完全没遇到认证问题。
注意:如果部署到云服务器时用的是宝塔面板自带 MySQL 5.7,你的 JDBC 连接字符串需要调整一下,驱动类从 com.mysql.jdbc.Driver 换成 com.mysql.cj.jdbc.Driver,同时注意时区参数 serverTimezone=Asia/Shanghai。
2.5 技术栈的补充组件
除了三大核心组件,我还引入了几个补充组件,它们在项目里扮演了重要角色:
- Redis:用来缓存短信验证码、存储 JWT 黑名单、缓存服务项目和热门宠物门店的基础数据。Redis 的高并发读写能力保证了这些高频数据的访问性能。
- JWT:无状态登录令牌,用户登录成功后签发 Token,前端每次请求在 header 里带上,后端通过拦截器统一校验。用 JWT 的另一个好处是天然支持多端登录,不需要在服务端维护 Session。
- Aliyun OSS / 本地文件存储:宠物照片、服务完成后的现场照片上传。我做了统一的文件上传接口,根据配置文件切换存储方式,本地开发用本地磁盘,部署到服务器后可以换成 OSS。
- Spring Task 定时任务:处理超时未接单的订单自动取消、服务完成后超过 24 小时未确认的订单自动完结等逻辑。
3. 数据库设计:核心表与订单状态机
3.1 核心表结构总览
这个项目的数据库我一共设计了 12 张核心表,覆盖用户、宠物、服务、订单、评价、消息等所有业务模块。
用户相关的表有用户表(包括宠物主和遛宠师两个角色的公共字段)、遛宠师审核信息表、宠物档案表。服务相关的表有服务项目分类表、订单表、订单状态流转记录表。交易相关的表有支付记录表、退款记录表。互动相关的表有评价表、消息通知表、收藏表。再加上一个管理员表,总共 12 张。
这种表结构设计在需求层面是完整自洽的,不存在那种“为了凑表而造表”的情况。举几个核心表的关键字段来说:
用户表(user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 用户名,唯一 |
| password | varchar(255) | 加密后的密码(BCrypt) |
| phone | varchar(20) | 手机号,唯一索引 |
| role | varchar(20) | 角色:ROLE_USER / ROLE_CAREGIVER / ROLE_ADMIN |
| status | tinyint | 状态:0禁用、1正常 |
| avatar | varchar(255) | 头像 URL |
| create_time | datetime | 注册时间 |
宠物表(pet)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 所属用户 ID |
| name | varchar(50) | 宠物名 |
| category | varchar(20) | 猫 / 狗 / 其他 |
| breed | varchar(50) | 品种 |
| weight | decimal(5,2) | 体重 kg |
| age | int | 年龄 |
| personality | varchar(255) | 性格说明,比如胆小、粘人 |
| feeding_note | varchar(500) | 喂食注意事项 |
| default_flag | tinyint | 是否是默认宠物 |
服务项目表(service_item)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(100) | 服务名称,如“上门喂猫一次” |
| category | varchar(50) | 分类:喂食 / 遛狗 / 陪玩 / 铲屎 |
| price | decimal(10,2) | 服务价格 |
| unit | varchar(20) | 计价单位:次 / 小时 |
| description | text | 服务说明 |
| status | tinyint | 是否上架 |
3.2 订单表:整个系统的核心枢纽
订单表是整个系统最重要的表,几乎所有业务操作都在围绕订单状态流转展开。我在设计时把核心字段都梳理清楚了:
- 订单编号:使用业务编号而非数据库自增 ID 作为对外展示的编号,格式类似
CG2024051210001(CG 表示宠物服务,后面是时间戳加随机数),这样用户在客服咨询时报编号更方便,也避免了暴露系统的真实订单量。 - 下单信息:下单用户 ID、遛宠师 ID(接单后写入)、服务项目 ID。
- 服务信息:宠物 ID、服务地址(省市区 + 详细地址)、服务开始时间、服务时长。
- 金额信息:订单金额、优惠金额、实付金额。这里我坚持把三个金额都存下来,因为后续如果要做订单对账,分开记录才不会算糊涂账。
- 状态字段:订单状态、退款状态(0无、1申请中、2已退款、3拒绝)。
- 时间字段:下单时间、接单时间、服务开始时间、服务完成时间、订单完结时间。每个时间点都有明确含义,后续统计遛宠师的服务时长和响应速度完全有据可查。
订单金额计算我放在了后端完成:订单金额 = 服务项目单价 × 服务时长/次数,再减去优惠金额。前端下单页面只是做数据的展示和提交,不在前端做金额计算,避免因为计算逻辑不一致导致的价格错误。
3.3 订单状态机的设计与流转
订单状态是整个业务的核心灵魂,我在代码里用枚举类型来管理,状态定义如下:
- 0 待接单:宠物主下单成功,等待遛宠师接单。这个状态下用户可以取消订单,遛宠师可以在接单大厅看到订单。
- 1 已接单:遛宠师接单成功。此时双方建立契约关系,任何一方都不能随意取消,如果遛宠师需要取消必须申请平台介入。
- 2 服务中:遛宠师已到达服务地点,点击开始服务。这个动作同时会推送消息给宠物主,让主人知道服务已在进行中。
- 3 待确认:遛宠师完成服务,上传服务照片后订单进入待确认状态。宠物主有 24 小时确认窗口,如果超时未确认,系统自动确认碰单完成。
- 4 已完成:订单完结,双方可以互相评价。此时宠物主的资金(虚拟账户中的金额)才会正式结算给遛宠师。
- 5 已取消:订单被取消。要区分是宠物主在待接单状态主动取消,还是系统超时自动取消。
这个状态机的设计原则是:每个状态的变更都必须有明确的操作者和触发条件。比如从“待接单”到“已接单”,只有遛宠师角色能够触发,而且必须是订单状态等于 0 的情况下才能变更。我在 Service 层的实现里写了一个状态变更的校验方法,任何非法状态跳转都会被拦截,从机制上避免脏数据。
3.4 索引设计与 SQL 优化
订单表和用户表的数据规模随业务增长会越来越大,索引设计不合理的话,查询会越来越慢。我在设计表的时候就把索引规划进去了:
- 订单表:联合索引
(user_id, status)用于宠物主查询自己的订单列表;联合索引(caregiver_id, status)用于遛宠师的接单列表;单列索引(order_no)用于客服按单号查询。 - 用户表:唯一索引
(phone)保证手机号不重复,同时手机号登录时可以快速定位用户。 - 评价表:联合索引
(order_id)保证一笔订单只能有一条评价,通过唯一约束实现。
SQL 优化方面,我踩过的一个比较典型的坑是遛宠师接单大厅的列表查询。最早我直接用订单表全表扫描然后排序,数据量一上来就慢。后来我在查询条件里加了“订单状态 = 待接单”和“服务区域匹配”两个过滤条件,配合 (status, service_city) 联合索引,查询性能改善非常明显。另外在写动态 SQL 的时候,我使用了 <where> 标签而不是手动拼接 WHERE 1=1,这样既避免了 SQL 注入风险,也不会产生多余的 AND 关键字。
4. 后端核心模块实现思路
4.1 项目分层结构与包设计
后端项目的包结构我按照职责进行了清晰的划分,采用经典的 Controller-Service-Mapper 三层结构:
code复制com.petcare
├── common // 通用工具类、常量、统一返回结果、异常处理
├── config // WebMvc 配置、拦截器注册、跨域配置、Redis 配置
├── controller // 接口层,只做参数接收和结果返回
├── service // 业务逻辑层,核心业务判断都在这层
│ └── impl // 接口实现类
├── mapper // MyBatis 接口,对应 XML 文件
├── entity // 数据库实体类
├── dto // 前端传入的数据封装对象
├── vo // 返回给前端的数据封装对象
├── enums // 订单状态、角色、支付状态等枚举
├── exception // 自定义异常类
└── interceptor // 登录拦截器、权限拦截器
实体类(entity)和 VO 分开是我比较坚持的一点。数据库实体类里的字段和数据库表一一对应,比如用户表里的密码字段、创建时间字段,这些不应该直接返回给前端。VO 是接口返回给前端的数据格式,只包含前端需要的字段,比如用户信息返回时会把密码抹掉。这样做的好处是接口返回的数据干净整洁,不会暴露敏感信息。
4.2 用户登录与 JWT 鉴权
用户登录走的是手机号 + 密码(验证码功能我预留了通道)的方式。密码加密用的是 BCrypt,每次用户输入的密码经过 BCrypt 算法生成哈希值,再和数据库里的哈希值做比对。为什么要用 BCrypt 而不是简单的 MD5?因为 MD5 已经被大规模破解,彩虹表攻击对 MD5 是致命打击,而 BCrypt 内置盐值并且算法设计上就是慢哈希,暴力破解成本非常高。
登录成功后,后端签发 JWT 令牌返回给前端。JWT 令牌里我放了三部分信息:用户 ID、角色、过期时间。令牌的有效期设置的是 24 小时,前端每次请求在 header 的 Authorization 字段带上 Bearer token,后端拦截器解析令牌获取用户信息,存到 ThreadLocal 里的 UserContext 中,后续业务逻辑直接通过 UserContext.getCurrentUserId() 获取当前登录用户,不需要每次从数据库里查。
权限控制上,我注册了拦截器统一处理:
- 登录拦截:所有需要登录的接口(除了登录、注册、查询服务项目等公开接口)都走登录校验。
- 角色拦截:根据接口的访问权限要求,校验当前用户的角色是否匹配。遛宠师接单的接口只允许 ROLE_CAREGIVER 访问,管理后台的接口只允许 ROLE_ADMIN 访问。
注意:JWT 的无状态特性带来了一个安全盲区——如果用户的令牌被窃取,在令牌过期之前是无法主动失效的。为了解决这个问题,我在 Redis 里维护了一个令牌黑名单,用户主动退出时把令牌加入黑名单,拦截器校验的时候先查黑名单再验签。
4.3 订单流程实现的关键细节
订单模块是后端代码量最大、逻辑最复杂的部分。以“宠物主下单 → 遛宠师接单 → 服务完成 → 订单确认”这条主线,我把几个关键细节展开讲讲:
下单接口
下单接口接收的参数有服务项目 ID、宠物 ID、服务地址、服务时间、备注等。下订单的 Service 方法上加了一个 @Transactional 事务注解,保证以下操作要么全部成功要么全部回滚:生成订单记录、扣减用户虚拟账户余额(如果使用余额支付)、发送通知消息。
下单前还需要校验几个前置条件:宠物是否属于当前用户、服务项目是否上架、用户账户状态是否正常。校验不通过直接抛出自定义业务异常,由全局异常处理器统一格式返回给前端。
接单接口
遛宠师接单是整个系统并发压力最大的点。多个遛宠师可能同时看到同一个订单,如果都用“先查订单状态,再更新订单状态”的方式,就会出现两个人同时接单成功的超卖问题。我在接单的 SQL 里用了乐观锁的思路:
sql复制UPDATE orders SET caregiver_id = ?, status = 1
WHERE id = ? AND status = 0
这条 SQL 的 where 条件里带了 status = 0,数据库层面保证只有第一个执行成功的遛宠师能把状态从 0 更新到 1,第二个人的 update 影响行数为 0,代码里判断影响行数就知道接单失败了。这个方案比用分布式锁简单得多,而且完全可靠。
服务完成接口
遛宠师上传服务照片并点击“完成服务”,订单状态从“服务中”变为“待确认”。这里我做了一个重要的业务校验:只有订单状态为“服务中”、操作人是该订单的接单遛宠师,才能提交完成操作。同时照片上传走的是单独的文件上传接口,完成服务时只传照片 URL 列表。
订单确认接口
宠物主看到订单进入“待确认”状态后,可以选择确认完成或者发起申诉。确认完成后订单变为“已完成”,同时触发两个后续动作:给遛宠师账号增加本次服务收入,生成评价提醒。这里我把“订单完成”和“资金结算”放在同一个事务里,避免了订单已经完成了但资金没到账的数据不一致问题。
4.4 定时任务与消息通知
系统里有两类定时任务,都是通过 Spring 的 @Scheduled 注解实现的:
超时未接单自动取消
用户下单后如果 30 分钟内没有遛宠师接单,系统自动取消订单并通知用户。实现方式是每分钟扫描一次订单表,找出“状态为待接单且下单时间超过 30 分钟”的订单,批量取消。为了保证定时任务不会因为异常中断导致扫描遗漏,我在代码里加了任务执行日志,方便排查。
超时未确认自动完成
服务完成后,如果宠物主 24 小时内没有确认也没有申诉,系统自动把订单置为已完成并完成资金结算。这个逻辑对遛宠师来说很友好,不用担心宠物主故意拖延不确认导致自己拿不到钱。
消息通知模块我用了站内信 + 微信模板消息预告的方案。站内信实现简单,用户登录后拉取未读消息列表,阅读后标记已读。微信模板消息是预留接口,真正接入需要申请微信服务号模板权限,属于可选项。
4.5 文件上传与存储
宠物照片、遛宠师的入驻资质照片、服务完成现场照片,都需要文件上传能力。我做了一个统一的文件上传接口,接收 MultipartFile 类型参数,然后根据配置文件里的存储类型判断是存本地还是存 OSS。
本地存储的方式注意几个问题:文件保存路径需要是绝对路径,而且和项目部署目录要分开,避免重新部署时文件被覆盖;返回给前端的 URL 需要通过映射配置把本地磁盘路径映射成 /files/** 的访问路径。OSS 的方式就更简单了,客户端直接调用 OSS 的 SDK 上传,返回文件的公网访问 URL。
5. 前端 Vue 实现与前后端联调要点
5.1 前端项目结构与页面规划
前端我按照 Vue 2 标准工程化结构来组织:
code复制src
├── api // 接口请求封装,按模块拆分文件
├── assets // 静态资源、全局样式
├── components // 公共组件(上传组件、地址选择、时间选择)
├── router // 路由配置
├── store // 状态管理(Vuex)
├── utils // 请求封装、工具函数
├── views // 页面组件
│ ├── user // 宠物主相关页面
│ ├── caregiver // 遛宠师相关页面
│ ├── admin // 管理后台页面
│ └── common // 公共页面(首页、登录页、注册页)
页面规划上,用户端的核心页面有:首页(服务分类展示)、服务详情页(价格、服务说明、下单入口)、下单确认页(选宠物、填地址、选时间)、订单列表页(按状态分组 tab)、订单详情页(订单状态流转时间线)、宠物管理页、个人中心页。遛宠师端的核心页面有:接单大厅(可接单列表)、我的接单(各状态订单)、收入统计页、入驻申请页。管理后台有:数据概览、用户管理(列表查询、禁用/启用)、遛宠师审核(资质审核、通过/驳回)、订单管理(订单查询、异常订单处理)、服务项目管理(上架/下架、价格调整)。
5.2 axios 封装与接口管理
axios 请求封装是前后端联调最关键的一环。我在 utils/request.js 里做了统一封装,重点处理了四件事:
第一是 baseURL 配置。开发环境通过 Vite 或者 Vue CLI 的代理配置把 /api 开头的请求转发到后端服务,生产环境由 Nginx 统一转发。我把 baseURL 写成了 /api,整个项目的接口路径都是 /api/user/login 这种格式,这样不管部署到什么环境,只需要调整代理配置即可。
第二是请求拦截器。每次请求发出之前,从 Vuex 里读取 token,如果有就在 header 里加上 Authorization: Bearer ${token}。
第三是响应拦截器。后端统一返回的 JSON 结构是 { code: 200, message: "success", data: {...} }。响应拦截器判断 code 是否等于 200,如果不是就把 message 用 Element UI 的 Message 组件弹出错误提示。如果 code 是 401(token 过期或未登录),就跳转到登录页并清除本地登录状态。
第四是全局 loading 控制。我在请求开始和结束的时候通过一个计数器控制全局 loading 的显示和隐藏,避免多个请求并发时 loading 提前关闭。
5.3 路由守卫与权限控制
前端路由分为三类:不需要登录的公开页面(首页、登录页、注册页)、需要登录的页面(下单页、订单中心、个人中心)、需要特定角色的页面(遛宠师接单大厅、管理后台)。
我用 Vue Router 的全局前置守卫做统一控制:
javascript复制router.beforeEach((to, from, next) => {
const token = store.state.user.token
const role = store.state.user.role
if (to.meta.requiresAuth && !token) {
next({ path: '/login', query: { redirect: to.fullPath }})
return
}
if (to.meta.role && to.meta.role !== role) {
next({ path: '/403' })
return
}
next()
})
这里要注意一个细节:路由守卫只能控制页面跳转,接口层面的权限校验必须依靠后端拦截器。前端控制权限只是用户体验层面的优化,不能作为安全措施,因为技术上用户可以绕过前端直接调用后端接口。所以我在写接口的时候,每个需要权限的接口都严格校验了当前用户的角色。
5.4 前后端联调的常见问题
前后端联调阶段最容易遇到的坑,我整理几个真实的:
- 跨域问题:开发环境前端跑在 8080 端口,后端跑在 8081 端口,浏览器会拦截跨域请求。解决办法是在 Vue CLI 的 devServer 里配置代理,把 /api 转发到 8081,同时关闭后端的 CORS 限制,或者后端配置一下允许跨域。
- 时间格式化问题:Java 后端返回的 LocalDateTime 默认序列化格式是 "2024-05-12T10:30:00",前端想要的是 "2024-05-12 10:30:00"。我在后端的统一配置里加了 Jackson 的日期格式化,同时在前端写了一个日期格式化工具函数双保险。
- Long 类型精度丢失:数据库 ID 是 bigint,前端 JavaScript 的 Number 类型在超过 2^53 时会丢失精度。订单 ID 如果直接返回到前端,在详情页跳转传参时会出现 ID 对不上。解决办法是在后端把 Long 类型序列化成 String,加上 Jackson 的 ToStringSerializer 配置。
- 空值处理:后端返回的 null 字段,前端如果不做兜底处理,页面会显示 undefined。我在全局响应拦截器里对 data 做了空值判断,同时前端模板里统一用
|| '--'处理展示。
6. 部署全流程实录
6.1 本地开发环境准备
项目要跑起来,本地环境需要以下配置:
- JDK 1.8+:我用的是 JDK 1.8,SpringBoot 2.7.x 对 JDK 8 的支持非常成熟。
- Maven 3.6+:用来管理后端依赖和打包。
- MySQL 8.0:本地开发数据库,我用 Docker 起了一个 MySQL 8.0 容器,命令行连接导入初始化 SQL。
- Redis 5+:本地开发用 Docker 或者直接装 Windows 版 Redis。
- Node.js 14+:前端构建需要,我用的是 16.20.0。
- IDE:后端用的 IntelliJ IDEA,前端用 VS Code,这个看个人习惯。
数据库初始化的时候,我准备了一个 init.sql 脚本,包含建库语句、建表语句、初始数据。初始数据包括管理员账号、服务项目的几条示例数据、测试用的宠物主账号和遛宠师账号(密码都是加密后的固定值)。首次部署的时候直接执行这个脚本就能把数据库搭起来,不需要手动建表。
6.2 后端打包与启动
后端打包非常简单,项目根目录执行:
bash复制mvn clean package -DskipTests
打包成功后在 target 目录下生成 pet-server.jar,通过以下命令启动:
bash复制java -jar pet-server.jar --spring.profiles.active=prod
这里我用 Spring 的多环境配置,把开发环境和生产环境的配置分开。application.yml 是公共配置,application-dev.yml 里的数据库地址是本地 IP,application-prod.yml 里的数据库地址是云服务器的内网 IP。
生产环境启动的时候,我用了 nohup 让进程在后台运行:
bash复制nohup java -Xms512m -Xmx512m -jar pet-server.jar --spring.profiles.active=prod > server.log 2>&1 &
-Xms 和 -Xmx 设置成 512m 是因为这个小项目的内存占用并不大,服务器是 2C4G 的配置,512m 只给予 JVM 足够了,剩余的内存留给 MySQL 和 Nginx。
6.3 前端构建与 Nginx 部署
前端构建命令:
bash复制npm install
npm run build
构建完成后 dist 目录里就是纯静态文件。我把它上传到服务器的 /var/www/pet-web 目录下,然后配置 Nginx:
nginx复制server {
listen 80;
server_name pet.example.com;
root /var/www/pet-web;
index index.html;
# 前端路由 history 模式配置,所有路径都回到 index.html
location / {
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;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 上传文件的访问映射
location /files/ {
alias /data/upload/;
}
}
这段配置里有三个关键点。第一个是 try_files $uri $uri/ /index.html,这是 Vue Router history 模式必须的配置,否则用户在刷新某个子页面路径的时候会报 404。第二个是 /api/ 的代理转发,前端请求的 /api/user/login 会被转发到后端服务的 /api/user/login,所以后端接口的 context-path 需要保持一致,我这里在后端配置里设置了 server.servlet.context-path=/api。第三个是 /files/ 的静态资源映射,把上传的图片通过 Nginx 直接返回,不用经过 Java 应用,性能更好。
6.4 服务器部署的整体方案
我用的部署方案是单台云服务器搞定所有组件:
- MySQL 8.0:负责数据存储。
- Redis:负责缓存和 token 黑名单。
- SpringBoot 应用:监听 8081 端口。
- Nginx:监听 80 端口,同时承担静态文件服务和反向代理两个角色。
- 上传文件目录:统一放在 /data/upload 下,方便备份。
服务器整体配置是 2C4G 的入门云服务器,Ubuntu 22.04 系统。对于这个项目的访问量来说完全够用了。如果后续业务量上来,可以考虑把 MySQL 和 Redis 单独部署到更高配的机器上,应用层做多实例部署加 Nginx 负载均衡,这是后话。
6.5 数据库初始化与数据备份
部署 MySQL 后,第一件事是把本地的 init.sql 导入到线上库:
bash复制mysql -u root -p < init.sql
导入后验证几个关键点:管理员账号能不能登录、服务项目数据是否齐全、测试账号是否能正常下单。我建议在正式上线前把测试数据清掉,只保留管理员账号和服务项目数据,避免把测试垃圾数据暴露给真实用户。
数据库备份我写了一个简单的 shell 脚本,每天凌晨用 crontab 定时执行 mysqldump 全量备份,保留最近 7 天的备份文件。备份文件通过 scp 传回本地或者传到对象存储,防止服务器磁盘损坏导致数据全丢。这个习惯不管项目大小都建议养成,数据丢了任何代码都救不回来。
7. 常见问题排查与避坑实录
7.1 MyBatis 相关问题的排查思路
问题一:查询结果字段全是 null
这个问题的原因是 MyBatis 的 map-underscore-to-camel-case 配置没有开启,数据库的 create_time 字段无法自动映射到实体类的 createTime 属性。在 application.yml 里加上:
yaml复制mybatis:
configuration:
map-underscore-to-camel-case: true
问题二:动态 SQL 拼接出语法错误
<if> 标签判断的字段和数据库的字段搞混了,比如 XML 里写的是 user_id != null,但实际上传入 DTO 里的字段名是 userId。我的排查方法是在开发环境的 MyBatis 配置里开启 SQL 日志打印:
yaml复制logging:
level:
com.petcare.mapper: debug
控制台会打印完整的 SQL 语句,一眼就能看出来 SQL 拼错了哪里。
问题三:批量操作性能差
项目里有一处需要导入大量测试数据的地方,最开始客户端逐条插入,耗时非常长。后来改写成了 MyBatis 的 batch 执行器,用 SqlSession 的 batch 模式配合 foreach 标签批量插入,性能提升非常明显。不过批量操作要注意事务不能太大,我控制在每批 1000 条。
7.2 跨域问题的最终解决方案
我在本地联调时遇到的一个典型场景是:前端运行在 http://localhost:8080,后端运行在 http://localhost:8081,前端 axios 请求后端接口时浏览器报跨域错误。
解决方案是在后端写一个 WebMvcConfigurer 的配置类,允许所有来源跨域访问:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
注意解决跨域不能只在后端配 CORS,前端的开发代理也是必需的。因为浏览器拦截的是“跨域请求”和“响应是否允许跨域访问”两个层面,代理模式从根源上规避了跨域问题。生产环境因为前后端同域,Nginx 代理转发也不存在跨域问题。
7.3 部署时可能出现的环境问题
JDK 版本问题:我本地开发用的是 JDK 17,但服务器上只装了 JDK 8,结果打出来的 jar 包在服务器上启动时报 UnsupportedClassVersionError。解决方法是让项目的 pom.xml 里指定 java.version 为 1.8,同时本地也用 JDK 8 编译,保证打包环境一致。
MySQL 连接失败:服务器上 MySQL 的端口默认只监听 127.0.0.1 的话,后端应用和 MySQL 在同一台服务器上没有问题,但如果后续要远程连接管理数据库就会失败。我在初始化 MySQL 的时候加了 bind-address = 0.0.0.0 配置,同时用防火墙和数据库授权 IP 限制来保证安全。
端口占用:后端应用启动失败,查看日志发现 8081 端口已经被占用。处理方法是用 lsof -i:8081 找出占用进程,确认没问题后杀掉重启。
7.4 安全层面的几个细节
项目上线前,我专门过了一遍安全细节,这里分享几个容易被忽略的点:
密码存储:绝对不能明文存储密码,也不用可逆加密。我用的 BCrypt,每注册一个用户生成一个随机的盐值,即使两个用户密码相同,存储的哈希值也不一样。
SQL 注入防护:MyBatis 的 #{} 是预编译参数,可以防止大部分 SQL 注入。但在使用 ${} 做动态排序字段拼接的时候,要严格校验传入的字段名是否在白名单里。我的代码里排序字段就固定支持更新时间、价格、距离三个白名单值。
接口防刷:登录接口、验证码接口容易被恶意刷。我给登录接口加了一个简单的防刷策略:同一个 IP 在一分钟内最多允许 10 次登录尝试,超过就临时封禁 5 分钟。这个策略用 Redis 的 INCR + EXPIRE 就能实现。
敏感数据脱敏:用户手机号在后端查询出库后,如果不是本人查看,需要做脱敏处理,中间四位用星号代替。管理后台查看用户列表时可以看到完整手机号,但遛宠师查看订单信息时只能看到脱敏后的手机号。
7.5 一个容易被忽视的性能坑
这个项目初期有个隐藏的性能问题:遛宠师接单大厅的订单列表接口,最开始每查询一次都会去把订单关联的用户地址、宠物信息、服务项目信息全部查出来。这些关联数据都是独立的查询,一个列表 20 条订单,就可能是 60 次数据库查询。数据量小的时候感觉不出来,数据量稍微大一点接口响应时间就飙到好几秒。
优化方式是我把列表查询改成了单条 SQL JOIN 联查,一次性把需要关联的字段查出来。MyBatis 的 <association> 和 <collection> 标签也能做关联映射,但性能和 JOIN 方案相比还是有差距。对于订单列表这种实时性要求高的场景,JOIN 是最简单可靠的方式。如果需要查的数据字段非常多,再考虑拆分成多个查询然后用代码组装。
这个项目从需求分析到部署上线,整个流程走下来,我对前后端分离开发的理解又深了一层。接口设计的时候多想一步,数据库设计的时候多考虑一层索引,部署的时候多验证一轮环境,后面就能少踩很多坑。最后再分享一个小技巧:部署前,把这套系统的每个角色、每个核心流程都从用户视角完整走一遍——管理员登录、遛宠师入驻审核、宠物主下单、遛宠师接单服务、双方评价,一个环节都不要漏。我第一次部署完上线测试时,就是因为只测了正常路径没测异常路径,结果漏掉了一个“服务中订单无法取消”的边界场景,上线当天就被用户吐槽了。现在每次交付项目,我都会列一个全流程的功能清单,逐个打钩验证,这个习惯帮我避免了很多线上事故。
